Skip to content

Commit 4a09c8c

Browse files
Merge pull request #427 from OSLL/352_DB_limitation
352 DB limitation
2 parents 8d3223d + f7011df commit 4a09c8c

File tree

11 files changed

+149
-7
lines changed

11 files changed

+149
-7
lines changed

app/api/files.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,15 @@ def upload_presentation() -> (dict, int):
145145
'message': 'Presentation file should not exceed {}MB.'.format(
146146
Config.c.constants.presentation_file_max_size_in_megabytes
147147
)
148-
}, 404
148+
}, 413
149+
# check if file can be stored (*2.5 is an estimate of pdf and preview)
150+
if not DBManager().check_storage_limit(request.content_length * 2.5):
151+
return {
152+
'message': "Not enough space in database to store file. "
153+
"Please contact at email: "
154+
f"<a href='mailto:{Config.c.bugreport.report_mail}'>{Config.c.bugreport.report_mail}</a>"
155+
}, 413
156+
149157
presentation_file = request.files['presentation']
150158

151159
# check extension and mimetype of file

app/mongo_models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,6 @@ class Logs(MongoModel):
156156
filename = fields.CharField()
157157
funcName = fields.CharField()
158158
lineno = fields.IntegerField()
159+
160+
class StorageMeta(MongoModel):
161+
used_size = fields.IntegerField()

app/mongo_odm.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from gridfs import GridFSBucket, NoFile
1212
from pymodm import connect
1313
from pymodm.connection import _get_db
14-
from pymodm.errors import ValidationError
14+
from pymodm.errors import ValidationError, DoesNotExist
1515
from pymodm.files import GridFSStorage
1616
from pymongo import ReturnDocument
1717
from pymongo.errors import CollectionInvalid
@@ -23,26 +23,35 @@
2323
RecognizedAudioToProcess,
2424
RecognizedPresentationsToProcess, Sessions,
2525
TaskAttempts, TaskAttemptsToPassBack, Tasks,
26-
Trainings, TrainingsToProcess)
26+
Trainings, TrainingsToProcess, StorageMeta)
2727
from app.status import (AudioStatus, PassBackStatus, PresentationStatus,
2828
TrainingStatus)
2929
from app.utils import remove_blank_and_none
3030

3131
logger = get_root_logger()
3232

33-
33+
BYTES_PER_MB = 1024*1024
3434

3535
class DBManager:
3636
def __new__(cls):
3737
if not hasattr(cls, 'init_done'):
3838
cls.instance = super(DBManager, cls).__new__(cls)
3939
connect(Config.c.mongodb.url + Config.c.mongodb.database_name)
4040
cls.instance.storage = GridFSStorage(GridFSBucket(_get_db()))
41+
cls.instance.max_size = float(Config.c.constants.storage_max_size_mbytes) * BYTES_PER_MB
4142
cls.init_done = True
4243
return cls.instance
4344

4445
def add_file(self, file, filename=uuid.uuid4()):
45-
return self.storage.save(name=filename, content=file)
46+
try:
47+
file.seek(0, os.SEEK_END)
48+
size = file.tell()
49+
file.seek(0)
50+
except:
51+
size = len(file)
52+
_id = self.storage.save(name=filename, content=file)
53+
self.update_storage_size(size)
54+
return _id
4655

4756
def read_and_add_file(self, path, filename=None):
4857
if filename is None:
@@ -67,7 +76,50 @@ def get_file(self, file_id):
6776
except (NoFile, ValidationError, InvalidId) as e:
6877
logger.warning('file_id = {}, {}.'.format(file_id, e))
6978
return None
70-
79+
80+
def _get_or_create_storage_meta(self):
81+
try:
82+
return StorageMeta.objects.get({})
83+
except DoesNotExist:
84+
meta = StorageMeta(used_size=0).save()
85+
return meta
86+
87+
def get_used_storage_size(self):
88+
return self._get_or_create_storage_meta().used_size
89+
90+
def set_used_storage_size(self, size):
91+
meta = self._get_or_create_storage_meta()
92+
meta.used_size = size
93+
meta.save()
94+
95+
def update_storage_size(self, deltasize):
96+
meta = self._get_or_create_storage_meta()
97+
meta.used_size += deltasize
98+
meta.save()
99+
100+
def get_max_size(self):
101+
return self.max_size
102+
103+
# returns Bool variable - True if file can be stored else False
104+
def check_storage_limit(self, new_file_size):
105+
current_size = self.get_used_storage_size()
106+
inf_msg = (
107+
f"Check for ability to add file: "
108+
f"Current: {current_size/BYTES_PER_MB:.2f} MB, "
109+
f"New file: {new_file_size/BYTES_PER_MB:.2f} MB, "
110+
f"Storage size: {self.max_size/BYTES_PER_MB} MB"
111+
)
112+
logger.info(inf_msg)
113+
return False if current_size + new_file_size > self.max_size else True
114+
115+
def recalculate_used_storage_data(self):
116+
total_size = 0
117+
db = _get_db()
118+
for file_doc in db.fs.files.find():
119+
total_size += file_doc['length']
120+
self.set_used_storage_size(total_size)
121+
logger.info(f"Storage size recalculated: {total_size/BYTES_PER_MB:.2f} MB")
122+
71123

72124
class TrainingsDBManager:
73125
def __new__(cls):

app/routes/capacity.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from flask import Blueprint, render_template
2+
from app.mongo_odm import DBManager
3+
from app.root_logger import get_root_logger
4+
from app.lti_session_passback.auth_checkers import is_admin
5+
6+
logger = get_root_logger()
7+
8+
BYTES_PER_MB = 1024*1024
9+
10+
routes_capacity = Blueprint(
11+
'routes_capacity', __name__, url_prefix='/capacity')
12+
13+
@routes_capacity.route('/', methods=['GET'])
14+
def storage_capacity():
15+
if not is_admin():
16+
return {}, 404
17+
current_size = DBManager().get_used_storage_size()
18+
max_size = DBManager().get_max_size()
19+
ratio = current_size / max_size
20+
return render_template(
21+
'capacity.html',
22+
size=round(current_size / BYTES_PER_MB, 2),
23+
max_size=round(max_size / BYTES_PER_MB, 2),
24+
ratio=round(ratio * 100, 1)
25+
)
26+
27+
@routes_capacity.route('/refresh_capacity', methods=['POST'])
28+
def refresh_capacity():
29+
if not is_admin():
30+
return {}, 404
31+
DBManager().recalculate_used_storage_data()
32+
return {'message': 'OK'}

app/static/js/upload.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ $(function(){
2626
return;
2727
}
2828
$("#spinner").hide();
29+
30+
if (response.status == 413) {
31+
response.json().then(responseJson => {
32+
$("#alert").show();
33+
$("#error-text").html(responseJson["message"] || "Файл слишком большой или превышает лимит хранилища");
34+
});
35+
$('#button-submit')[0].value = button_value;
36+
37+
return;
38+
}
39+
2940
response.json().then(responseJson => {
3041
if (responseJson["message"] !== "OK"){
3142
$("#alert").show();

app/templates/admin.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
<a href="{{ url_for("routes_admin.view_dumps")}}" target="_blank">{{ t("Архивы БД") }}</a>
1414
<br>
1515
<a href="{{ url_for("routes_version.view_version")}}" target="_blank">{{ t("Версия") }}</a>
16+
<br>
17+
<a href="{{ url_for("routes_capacity.storage_capacity")}}" target="_blank">{{ t("Заполненность БД") }}</a>
1618
</div>
1719
{% endblock %}

app/templates/capacity.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{% extends 'base.html' %}
2+
3+
{% block content %}
4+
<div class="container mt-4">
5+
<div class="card">
6+
<div class="card-body">
7+
<h4 class="card-title">Загруженность Базы Данных</h4>
8+
<p><strong>Использовано:</strong> {{ size }} Мбайт</p>
9+
<p><strong>Максимум:</strong> {{ max_size }} Мбайт</p>
10+
<p><strong>Заполнено:</strong> {{ ratio }}%</p>
11+
<div class="progress">
12+
<div class="progress-bar" role="progressbar" style="width: {{ ratio }}%;" aria-valuenow="{{ ratio }}" aria-valuemin="0" aria-valuemax="100">
13+
{{ ratio }}
14+
</div>
15+
</div>
16+
</div>
17+
<button type="button" id="refresh_button">Обновить данные</button>
18+
19+
<script src="/static/js/libraries/jquery.min.js"></script>
20+
<script>
21+
$('#refresh_button').click(function() {
22+
$.post("{{ url_for('routes_capacity.refresh_capacity') }}", {}, function(data) {
23+
location.reload();
24+
});
25+
});
26+
</script>
27+
</div>
28+
</div>
29+
{% endblock %}

app/web_speech_trainer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from app.routes.trainings import routes_trainings
3232
from app.routes.task_attempts import routes_task_attempts
3333
from app.routes.version import routes_version
34+
from app.routes.capacity import routes_capacity
3435
from app.status import PassBackStatus, TrainingStatus
3536
from app.training_manager import TrainingManager
3637
from app.utils import ALLOWED_EXTENSIONS, DEFAULT_EXTENSION
@@ -55,6 +56,7 @@
5556
app.register_blueprint(routes_trainings)
5657
app.register_blueprint(routes_task_attempts)
5758
app.register_blueprint(routes_version)
59+
app.register_blueprint(routes_capacity)
5860

5961
logger = get_root_logger(service_name='web')
6062

app_conf/config.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ lti_consumer_key=secretconsumerkey
55
lti_consumer_secret=supersecretconsumersecret
66
version_file=VERSION.json
77
backup_path=../dump/database-dump/
8+
storage_max_size_mbytes=20000
89

910
[mongodb]
1011
url=mongodb://db:27017/

app_conf/testing.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ lti_consumer_key=testing_lti_consumer_key
55
lti_consumer_secret=testing_lti_consumer_secret
66
version_file=VERSION.json
77
backup_path=../dump/database-dump/
8+
storage_max_size_mbytes=20000
89

910
[mongodb]
1011
url=mongodb://db:27017/

0 commit comments

Comments
 (0)