component file sharing: quotas implementation:
quotas can now be specified using the `quotas_json` option of `component file_sharing`
section in settings. This must be a dict where:
- `users` key contains default quotas for all users
- `admins` key contains quotas for administrators (not implemented yet)
- `jids` contain bare JID to quota mapping, to have user-specific quota
The value can be either a int for quota in bytes, or a case insensitive string with an
optional multiplier symbol (e.g. "500 Mio"). `None` can be used for explicit unlimited
quota (which is the default is `users` is not set).
When a file size is too big for quota, upload is refused with an error message indicating
allowed quota, used space, and the size of the file that user wants to upload.
This commit is contained in:
parent
8ed9e310c5
commit
ac8f9f7817
1 changed files with 68 additions and 16 deletions
|
|
@ -25,7 +25,7 @@ import shortuuid
|
|||
import unicodedata
|
||||
from urllib.parse import urljoin, urlparse, quote, unquote
|
||||
from pathlib import Path
|
||||
from sat.core.i18n import _
|
||||
from sat.core.i18n import _, D_
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core import exceptions
|
||||
from sat.core.log import getLogger
|
||||
|
|
@ -34,6 +34,7 @@ from sat.tools import video
|
|||
from sat.tools.common import regex
|
||||
from sat.tools.common import uri
|
||||
from sat.tools.common import files_utils
|
||||
from sat.tools.common import utils
|
||||
from sat.tools.common import tls
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.words.protocols.jabber import error
|
||||
|
|
@ -75,6 +76,10 @@ COMMENT_NODE_PREFIX = "org.salut-a-toi.file_comments/"
|
|||
# Directory used to buffer request body (i.e. file in case of PUT) we use more than one @
|
||||
# there, to be sure than it's not conflicting with a JID
|
||||
TMP_BUFFER_DIR = "@@tmp@@"
|
||||
OVER_QUOTA_TXT = D_(
|
||||
"You are over quota, your maximum allowed size is {quota} and you are already using "
|
||||
"{used_space}, you can't upload {file_size} more."
|
||||
)
|
||||
|
||||
server.version = unicodedata.normalize(
|
||||
'NFKD',
|
||||
|
|
@ -242,7 +247,7 @@ class FileSharingRequest(server.Request):
|
|||
if self._upload_data is not None:
|
||||
return self._upload_data
|
||||
|
||||
# self.path is not available if we are easly in the request (e.g. when gotLength
|
||||
# self.path is not available if we are early in the request (e.g. when gotLength
|
||||
# is called), in which case channel._path must be used. On the other hand, when
|
||||
# render_[VERB] is called, only self.path is available
|
||||
path = self.channel._path if self.path is None else self.path
|
||||
|
|
@ -347,7 +352,8 @@ class FileSharing:
|
|||
self._jf = self.host.plugins["XEP-0234"]
|
||||
self._h = self.host.plugins["XEP-0300"]
|
||||
self._t = self.host.plugins["XEP-0264"]
|
||||
self.host.plugins["XEP-0363"].registerHandler(self._onHTTPUpload)
|
||||
self._hu = self.host.plugins["XEP-0363"]
|
||||
self._hu.registerHandler(self._onHTTPUpload)
|
||||
self.host.trigger.add("FILE_getDestDir", self._getDestDirTrigger)
|
||||
self.host.trigger.add(
|
||||
"XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger, priority=1000
|
||||
|
|
@ -400,6 +406,17 @@ class FileSharing:
|
|||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
def getQuota(self, client, entity):
|
||||
"""Return maximum size allowed for all files for entity"""
|
||||
# TODO: handle special entities like admins
|
||||
quotas = self.host.memory.getConfig("component file_sharing", "quotas_json", {})
|
||||
entity_bare_s = entity.userhost()
|
||||
try:
|
||||
quota = quotas["jids"][entity_bare_s]
|
||||
except KeyError:
|
||||
quota = quotas.get("users")
|
||||
return None if quota is None else utils.parseSize(quota)
|
||||
|
||||
async def generate_thumbnails(self, extra: dict, image_path: Path):
|
||||
thumbnails = extra.setdefault(C.KEY_THUMBNAILS, [])
|
||||
for max_thumb_size in self._t.SIZES:
|
||||
|
|
@ -485,7 +502,7 @@ class FileSharing:
|
|||
extra=extra,
|
||||
)
|
||||
|
||||
def _getDestDirTrigger(
|
||||
async def _getDestDirTrigger(
|
||||
self, client, peer_jid, transfer_data, file_data, stream_object
|
||||
):
|
||||
"""This trigger accept file sending request, and store file locally"""
|
||||
|
|
@ -496,6 +513,19 @@ class FileSharing:
|
|||
assert C.KEY_PROGRESS_ID in file_data
|
||||
filename = file_data["name"]
|
||||
assert filename and not "/" in filename
|
||||
quota = self.getQuota(client, peer_jid)
|
||||
if quota is not None:
|
||||
used_space = await self.host.memory.fileGetUsedSpace(client, peer_jid)
|
||||
|
||||
if (used_space + file_data["size"]) > quota:
|
||||
raise error.StanzaError(
|
||||
"not-acceptable",
|
||||
text=OVER_QUOTA_TXT.format(
|
||||
quota=utils.getHumanSize(quota),
|
||||
used_space=utils.getHumanSize(used_space),
|
||||
file_size=utils.getHumanSize(file_data['size'])
|
||||
)
|
||||
)
|
||||
file_tmp_dir = self.host.getLocalPath(
|
||||
None, C.FILES_TMP_DIR, peer_jid.userhost(), component=True, profile=False
|
||||
)
|
||||
|
|
@ -597,19 +627,34 @@ class FileSharing:
|
|||
except KeyError:
|
||||
log.error(f"trying to purge an inexisting upload slot ({upload_id})")
|
||||
|
||||
def _onHTTPUpload(self, client, request):
|
||||
async def _onHTTPUpload(self, client, request):
|
||||
# filename should be already cleaned, but it's better to double check
|
||||
assert '/' not in request.filename
|
||||
# client._file_sharing_allowed_hosts is set in plugin XEP-0329
|
||||
if request.from_.host not in client._file_sharing_allowed_hosts:
|
||||
raise error.StanzaError("forbidden")
|
||||
|
||||
quota = self.getQuota(client, request.from_)
|
||||
if quota is not None:
|
||||
used_space = await self.host.memory.fileGetUsedSpace(client, request.from_)
|
||||
|
||||
if (used_space + request.size) > quota:
|
||||
raise error.StanzaError(
|
||||
"not-acceptable",
|
||||
text=OVER_QUOTA_TXT.format(
|
||||
quota=utils.getHumanSize(quota),
|
||||
used_space=utils.getHumanSize(used_space),
|
||||
file_size=utils.getHumanSize(request.size)
|
||||
),
|
||||
appCondition = self._hu.getFileTooLargeElt(max(quota - used_space, 0))
|
||||
)
|
||||
|
||||
upload_id = shortuuid.ShortUUID().random(length=30)
|
||||
assert '/' not in upload_id
|
||||
timer = reactor.callLater(30, self._purge_slot, upload_id)
|
||||
self.expected_uploads[upload_id] = (client, request, timer)
|
||||
url = urljoin(client._file_sharing_base_url, f"{upload_id}/{request.filename}")
|
||||
slot = self.host.plugins["XEP-0363"].Slot(
|
||||
slot = self._hu.Slot(
|
||||
put=url,
|
||||
get=url,
|
||||
headers=[],
|
||||
|
|
|
|||
Loading…
Reference in a new issue