diff options
| author | rr- | 2017-01-07 11:59:43 +0100 |
|---|---|---|
| committer | rr- | 2017-01-08 10:25:29 +0100 |
| commit | 036fa9ee3908f480b9e0f8aa3018bf7c8768e88f (patch) | |
| tree | c83823023ae078affbd0a17857ccb10f9948b106 /server | |
| parent | f00cc5f3fa3a16a1f7055c772c3008d030f41fef (diff) | |
server/uploads: add file upload api
Diffstat (limited to 'server')
| -rw-r--r-- | server/szurubooru/api/__init__.py | 1 | ||||
| -rw-r--r-- | server/szurubooru/api/upload_api.py | 10 | ||||
| -rw-r--r-- | server/szurubooru/errors.py | 4 | ||||
| -rw-r--r-- | server/szurubooru/facade.py | 16 | ||||
| -rw-r--r-- | server/szurubooru/func/file_uploads.py | 29 | ||||
| -rw-r--r-- | server/szurubooru/func/files.py | 6 | ||||
| -rw-r--r-- | server/szurubooru/rest/context.py | 30 |
7 files changed, 84 insertions, 12 deletions
diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index 308b86b..2a2d5af 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -6,3 +6,4 @@ import szurubooru.api.tag_category_api import szurubooru.api.comment_api import szurubooru.api.password_reset_api import szurubooru.api.snapshot_api +import szurubooru.api.upload_api diff --git a/server/szurubooru/api/upload_api.py b/server/szurubooru/api/upload_api.py new file mode 100644 index 0000000..eaf2880 --- /dev/null +++ b/server/szurubooru/api/upload_api.py @@ -0,0 +1,10 @@ +from szurubooru.rest import routes +from szurubooru.func import auth, file_uploads + + +@routes.post('/uploads/?') +def create_temporary_file(ctx, _params=None): + auth.verify_privilege(ctx.user, 'uploads:create') + content = ctx.get_file('content', required=True, allow_tokens=False) + token = file_uploads.save(content) + return {'token': token} diff --git a/server/szurubooru/errors.py b/server/szurubooru/errors.py index 33bdfce..f7edf85 100644 --- a/server/szurubooru/errors.py +++ b/server/szurubooru/errors.py @@ -36,6 +36,10 @@ class MissingRequiredFileError(ValidationError): pass +class MissingOrExpiredRequiredFileError(MissingRequiredFileError): + pass + + class MissingRequiredParameterError(ValidationError): pass diff --git a/server/szurubooru/facade.py b/server/szurubooru/facade.py index 5211ca8..e29aae1 100644 --- a/server/szurubooru/facade.py +++ b/server/szurubooru/facade.py @@ -1,11 +1,13 @@ ''' Exports create_app. ''' import os +import time import logging +import threading import coloredlogs import sqlalchemy.orm.exc from szurubooru import config, errors, rest -from szurubooru.func import posts +from szurubooru.func import posts, file_uploads # pylint: disable=unused-import from szurubooru import api, middleware @@ -79,6 +81,15 @@ def validate_config(): raise errors.ConfigError('Database is not configured') +def purge_old_uploads(): + while True: + try: + file_uploads.purge_old_uploads() + except Exception as ex: + logging.exception(ex) + time.sleep(60 * 5) + + def create_app(): ''' Create a WSGI compatible App object. ''' validate_config() @@ -88,6 +99,9 @@ def create_app(): if config.config['show_sql']: logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + purge_thread = threading.Thread(target=purge_old_uploads) + purge_thread.daemon = True + purge_thread.start() posts.populate_reverse_search() rest.errors.handle(errors.AuthError, _on_auth_error) diff --git a/server/szurubooru/func/file_uploads.py b/server/szurubooru/func/file_uploads.py new file mode 100644 index 0000000..95698e3 --- /dev/null +++ b/server/szurubooru/func/file_uploads.py @@ -0,0 +1,29 @@ +import datetime +from szurubooru.func import files, util + + +MAX_MINUTES = 60 + + +def _get_path(checksum): + return 'temporary-uploads/%s.dat' % checksum + + +def purge_old_uploads(): + now = datetime.datetime.now() + for file in files.scan('temporary-uploads'): + file_time = datetime.datetime.fromtimestamp(file.stat().st_ctime) + if now - file_time > datetime.timedelta(minutes=MAX_MINUTES): + files.delete('temporary-uploads/%s' % file.name) + + +def get(checksum): + return files.get('temporary-uploads/%s.dat' % checksum) + + +def save(content): + checksum = util.get_sha1(content) + path = _get_path(checksum) + if not files.has(path): + files.save(path, content) + return checksum diff --git a/server/szurubooru/func/files.py b/server/szurubooru/func/files.py index 4834045..3ca8777 100644 --- a/server/szurubooru/func/files.py +++ b/server/szurubooru/func/files.py @@ -16,6 +16,12 @@ def has(path): return os.path.exists(_get_full_path(path)) +def scan(path): + if has(path): + return os.scandir(_get_full_path(path)) + return [] + + def move(source_path, target_path): return os.rename(_get_full_path(source_path), _get_full_path(target_path)) diff --git a/server/szurubooru/rest/context.py b/server/szurubooru/rest/context.py index 6f74a4c..081064e 100644 --- a/server/szurubooru/rest/context.py +++ b/server/szurubooru/rest/context.py @@ -1,5 +1,5 @@ from szurubooru import errors -from szurubooru.func import net +from szurubooru.func import net, file_uploads def _lower_first(source): @@ -43,18 +43,26 @@ class Context: def get_header(self, name): return self._headers.get(name, None) - def has_file(self, name): - return name in self._files or name + 'Url' in self._params + def has_file(self, name, allow_tokens=True): + return (name in self._files + or name + 'Url' in self._params + or (allow_tokens and name + 'Token' in self._params)) - def get_file(self, name, required=False): + def get_file(self, name, required=False, allow_tokens=True): + ret = None if name in self._files: - return self._files[name] - if name + 'Url' in self._params: - return net.download(self._params[name + 'Url']) - if not required: - return None - raise errors.MissingRequiredFileError( - 'Required file %r is missing.' % name) + ret = self._files[name] + elif name + 'Url' in self._params: + ret = net.download(self._params[name + 'Url']) + elif allow_tokens and name + 'Token' in self._params: + ret = file_uploads.get(self._params[name + 'Token']) + if required and not ret: + raise errors.MissingOrExpiredRequiredFileError( + 'Required file %r is missing or has expired.' % name) + if required and not ret: + raise errors.MissingRequiredFileError( + 'Required file %r is missing.' % name) + return ret def has_param(self, name): return name in self._params |