Compare commits
No commits in common. "b423dab30c820e580b4939c37057879c2d676db3" and "35bc178845e03550c2229b4bc9e4ce221d4136b6" have entirely different histories.
b423dab30c
...
35bc178845
15
.drone.yml
15
.drone.yml
|
|
@ -1,15 +0,0 @@
|
||||||
kind: pipeline
|
|
||||||
name: default
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: unittest
|
|
||||||
image: python
|
|
||||||
commands:
|
|
||||||
- pip install -r requirements_test.txt
|
|
||||||
- ./scripts/run_tests.sh
|
|
||||||
|
|
||||||
settings:
|
|
||||||
username:
|
|
||||||
from_secret: gitea_username
|
|
||||||
password:
|
|
||||||
from_secret: gitea_password
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -4,5 +4,5 @@ lint:
|
||||||
stampreqs:
|
stampreqs:
|
||||||
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
|
|
||||||
stampreqsci:
|
test:
|
||||||
poetry export --without-hashes --with dev --format=requirements.txt > requirements_test.txt
|
pytest --cov=. --cov-report term-missing
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from common.models import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
class LockboxModelAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = Configuration.readonly_fields
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Configuration, LockboxModelAdmin)
|
||||||
|
|
@ -16,70 +16,24 @@ CONFIG_KEYS = {
|
||||||
"description": "Date created + this delta at which file expires",
|
"description": "Date created + this delta at which file expires",
|
||||||
"verbose_name": "File expiration delta (minutes)",
|
"verbose_name": "File expiration delta (minutes)",
|
||||||
"native_type": int,
|
"native_type": int,
|
||||||
"sensitive": False,
|
|
||||||
"default": 120,
|
"default": 120,
|
||||||
},
|
},
|
||||||
"ABANDONED_DELTA_MINUTES": {
|
"ABANDONED_DELTA_MINUTES": {
|
||||||
"description": "Date created + this delta at which a file is marked as abandoned",
|
"description": "Date created + this delta at which a file is marked as abandoned",
|
||||||
"verbose_name": "Uncompleted file abandoned max age",
|
"verbose_name": "Uncompleted file abandoned max age",
|
||||||
"native_type": int,
|
"native_type": int,
|
||||||
"sensitive": False,
|
|
||||||
"default": 20,
|
"default": 20,
|
||||||
},
|
},
|
||||||
"ABANDONED_EXPIRED_SCAN_INTERVAL": {
|
"ABANDONED_EXPIRED_SCAN_INTERVAL": {
|
||||||
"description": "Scan and scrub abandoned or expired uploads",
|
"description": "Scan and scrub abandoned or expired uploads",
|
||||||
"verbose_name": "Scan interval for abandoned/expired files",
|
"verbose_name": "Scan interval for abandoned/expired files",
|
||||||
"native_type": int,
|
"native_type": int,
|
||||||
"sensitive": False,
|
|
||||||
"default": 20,
|
"default": 20,
|
||||||
},
|
},
|
||||||
"MAX_CHUNK_BYTES": {
|
"MAX_UPLOAD_BYTES": {
|
||||||
"description": "Max bytes that can be uploaded in one go, too large of a value might result in a timeout",
|
"description": "Max bytes that can be uploaded in one go",
|
||||||
"verbose_name": "Max per chunk size in bytes",
|
|
||||||
"native_type": int,
|
|
||||||
"sensitive": False,
|
|
||||||
"default": 1024 * 1024 * 20, # 20 MB
|
|
||||||
},
|
|
||||||
"MAX_FILE_BYTES": {
|
|
||||||
"description": "Max total file size in bytes",
|
|
||||||
"verbose_name": "Max upload size in bytes",
|
"verbose_name": "Max upload size in bytes",
|
||||||
"native_type": int,
|
"native_type": int,
|
||||||
"sensitive": False,
|
"default": 2000000, # 2 MB
|
||||||
"default": 1024 * 1024 * 200, # 200 MB
|
|
||||||
},
|
|
||||||
"ENABLE_BROWSABLE_API": {
|
|
||||||
"description": "REST Framework browsable API is enabled (Always enabled if DEBUG is true)",
|
|
||||||
"verbose_name": "Enable browsable API",
|
|
||||||
"native_type": bool,
|
|
||||||
"sensitive": False,
|
|
||||||
"default": False,
|
|
||||||
},
|
|
||||||
"DEBUG": {
|
|
||||||
"description": "Django DEBUG flag is enabled (Do not use in production!)",
|
|
||||||
"verbose_name": "Django DEBUG flag value",
|
|
||||||
"native_type": bool,
|
|
||||||
"sensitive": False,
|
|
||||||
"default": False,
|
|
||||||
},
|
|
||||||
"SECRET_KEY": {
|
|
||||||
"description": "Django SECRET_KEY used for crypto singning",
|
|
||||||
"verbose_name": "Django SECRET_KEY",
|
|
||||||
"native_type": str,
|
|
||||||
"sensitive": True,
|
|
||||||
"default": None,
|
|
||||||
},
|
|
||||||
"ALLOWED_HOSTS": {
|
|
||||||
"description": "Where is this app being served from",
|
|
||||||
"verbose_name": "Allowed hosts",
|
|
||||||
"native_type": list,
|
|
||||||
"sensitive": False,
|
|
||||||
"default": ["*"],
|
|
||||||
},
|
|
||||||
"DB_SQLITE_ABSOLUTE_PATH": {
|
|
||||||
"description": "Path where db.sqlite3 is stored",
|
|
||||||
"verbose_name": "db.sqlite3 path",
|
|
||||||
"native_type": str,
|
|
||||||
"sensitive": False,
|
|
||||||
"default": ".",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-02-13 09:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Configuration',
|
||||||
|
fields=[
|
||||||
|
('lid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='lockbox ID')),
|
||||||
|
('date_created', models.DateTimeField(blank=True, help_text='date at which this object was created', verbose_name='date created')),
|
||||||
|
('date_updated', models.DateTimeField(blank=True, help_text='date at which this object was last updated', verbose_name='date updated')),
|
||||||
|
('key', models.CharField(choices=[('EXPIRATION_DELTA_MINUTES', 'Date created + this delta at which file expires'), ('ABANDONED_DELTA_MINUTES', 'Date created + this delta at which a file is marked as abandoned'), ('ABANDONED_EXPIRED_SCAN_INTERVAL', 'Scan and scrub abandoned or expired uploads'), ('MAX_UPLOAD_BYTES', 'Max bytes that can be uploaded in one go')], help_text='internal configuration key name', max_length=50)),
|
||||||
|
('value', models.CharField(help_text='actual DB config value', max_length=1024)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -4,6 +4,9 @@ from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.constants import CONFIG_KEYS
|
||||||
|
from common.utils import cast_to_native_type
|
||||||
|
|
||||||
|
|
||||||
class LockboxBase(models.Model): # pragma: no cover
|
class LockboxBase(models.Model): # pragma: no cover
|
||||||
lid = models.UUIDField(
|
lid = models.UUIDField(
|
||||||
|
|
@ -48,3 +51,54 @@ class LockboxBase(models.Model): # pragma: no cover
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.__class__.__name__} Object {self.lid}"
|
return f"{self.__class__.__name__} Object {self.lid}"
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key,
|
||||||
|
description,
|
||||||
|
verbose_name,
|
||||||
|
native_type,
|
||||||
|
default,
|
||||||
|
value=None,
|
||||||
|
source=None,
|
||||||
|
):
|
||||||
|
|
||||||
|
self.key = key
|
||||||
|
self.description = description
|
||||||
|
self.verbose_name = verbose_name
|
||||||
|
self.native_type = native_type
|
||||||
|
self.default = default
|
||||||
|
self.value = value
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(LockboxBase):
|
||||||
|
|
||||||
|
CONFIG_KEY_CHOICES = (
|
||||||
|
(key, value["description"]) for key, value in CONFIG_KEYS.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
key = models.CharField(
|
||||||
|
choices=CONFIG_KEY_CHOICES,
|
||||||
|
max_length=50,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
help_text=_("internal configuration key name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
value = models.CharField(
|
||||||
|
max_length=1024,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
help_text=_("actual DB config value"),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = LockboxBase.readonly_fields
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
native_type = CONFIG_KEYS[self.key]["native_type"]
|
||||||
|
cast_to_native_type(self.key, self.value, native_type)
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigSerializer(serializers.Serializer):
|
|
||||||
key = serializers.CharField(max_length=50)
|
|
||||||
description = serializers.CharField(max_length=120)
|
|
||||||
verbose_name = serializers.CharField(max_length=120)
|
|
||||||
native_type = serializers.CharField(max_length=10)
|
|
||||||
sensitive = serializers.BooleanField()
|
|
||||||
default = serializers.CharField(max_length=120)
|
|
||||||
value = serializers.CharField(max_length=120)
|
|
||||||
source = serializers.CharField(max_length=10)
|
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
if instance["default"] is not None:
|
|
||||||
instance["default"] = str(instance["default"])
|
|
||||||
|
|
||||||
if instance["sensitive"] and instance["value"] is not None:
|
|
||||||
instance["value"] = "***************"
|
|
||||||
|
|
||||||
elif instance["value"] is not None:
|
|
||||||
instance["value"] = str(instance["value"])
|
|
||||||
|
|
||||||
return super().to_representation(instance)
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from common import views_api
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("api/configs/", views_api.configs, name="api-config"),
|
|
||||||
path("api/configs/<str:key>/", views_api.configs, name="api-config"),
|
|
||||||
]
|
|
||||||
|
|
@ -1,33 +1,13 @@
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from typing import Any, NamedTuple
|
|
||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
|
|
||||||
from common.constants import CONFIG_KEYS
|
from common.constants import CONFIG_KEYS
|
||||||
|
|
||||||
|
|
||||||
class Config(NamedTuple):
|
|
||||||
key: str
|
|
||||||
description: str
|
|
||||||
verbose_name: str
|
|
||||||
native_type: type
|
|
||||||
sensitive: bool
|
|
||||||
default: Any
|
|
||||||
value: Any
|
|
||||||
source: str
|
|
||||||
|
|
||||||
def normalize_string(string, form="NFKC"):
|
def normalize_string(string, form="NFKC"):
|
||||||
return normalize(form, string)
|
return normalize(form, string)
|
||||||
|
|
||||||
def cast_to_native_type(key, value, native_type):
|
def cast_to_native_type(key, value, native_type):
|
||||||
|
|
||||||
if native_type == list:
|
|
||||||
value = value.split(",")
|
|
||||||
|
|
||||||
if native_type == bool:
|
|
||||||
if value == "false":
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return native_type(value)
|
return native_type(value)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
@ -37,21 +17,28 @@ def cast_to_native_type(key, value, native_type):
|
||||||
raise ValueError(message) from e
|
raise ValueError(message) from e
|
||||||
|
|
||||||
|
|
||||||
def get_config(key, value_only=True): # noqa: FBT002
|
def get_config(key):
|
||||||
default = CONFIG_KEYS[key]
|
from common.models import Config, Configuration
|
||||||
from_env = getenv(key)
|
config = Config(**CONFIG_KEYS[key], key=key)
|
||||||
|
|
||||||
if from_env:
|
obj = Configuration.objects.filter(key=key).first()
|
||||||
value = cast_to_native_type(key, from_env, default["native_type"])
|
|
||||||
source = "env"
|
|
||||||
else:
|
|
||||||
value = default["default"]
|
|
||||||
source = "default"
|
|
||||||
|
|
||||||
if value_only:
|
if obj:
|
||||||
return value
|
config.value = cast_to_native_type(key, obj.value, config.native_type)
|
||||||
return Config(key=key, value=value, source=source, **default)
|
config.source = "db"
|
||||||
|
return config
|
||||||
|
|
||||||
|
value = getenv(key)
|
||||||
|
|
||||||
|
if value:
|
||||||
|
config.value = cast_to_native_type(key, value, config.native_type)
|
||||||
|
config.source = "env_variable"
|
||||||
|
return config
|
||||||
|
|
||||||
|
config.value = config.default
|
||||||
|
config.source = "default"
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_max_size_chunk_bytes():
|
def get_max_size_chunk_bytes():
|
||||||
return get_config("MAX_CHUNK_BYTES")
|
return get_config("MAX_UPLOAD_BYTES").value
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.decorators import api_view
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from common.constants import CONFIG_KEYS
|
|
||||||
from common.serializers import ConfigSerializer
|
|
||||||
from common.utils import get_config
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_configs():
|
|
||||||
return [get_config(key, value_only=False)._asdict() for key in CONFIG_KEYS]
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
def configs(request, key=None):
|
|
||||||
if key:
|
|
||||||
if key not in CONFIG_KEYS:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
payload = ConfigSerializer(get_config(key, value_only=False)._asdict()).data
|
|
||||||
else:
|
|
||||||
payload = ConfigSerializer(get_all_configs(), many=True).data
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
payload,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
@ -1,23 +1,21 @@
|
||||||
"""Lockbox File Sharing"""
|
"""Lockbox File Sharing"""
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from common.utils import get_config
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
SECRET_KEY = get_config("SECRET_KEY")
|
SECRET_KEY = os.getenv("LOCKBOX_SECRET_KEY")
|
||||||
DEBUG = get_config("DEBUG")
|
# DEBUG = os.getenv("LOCKBOX_DEBUG")
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = get_config("ALLOWED_HOSTS")
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
ENABLE_BROWSABLE_API = os.getenv("ENABLE_BROWSABLE_API")
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
|
|
@ -66,9 +64,6 @@ WSGI_APPLICATION = 'lockbox.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
|
||||||
DB_SQLITE_ABSOLUTE_PATH = get_config("DB_SQLITE_ABSOLUTE_PATH")
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from common.utils import get_config
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
@ -7,11 +6,10 @@ from django.urls import include, path
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("storage/", include("storage.urls")),
|
path("storage/", include("storage.urls")),
|
||||||
path("common/", include("common.urls")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
if get_config("ENABLE_BROWSABLE_API"):
|
if settings.ENABLE_BROWSABLE_API:
|
||||||
urlpatterns.extend(path('api-auth/', include('rest_framework.urls')))
|
urlpatterns.extend(path('api-auth/', include('rest_framework.urls')))
|
||||||
|
|
||||||
urlpatterns.extend(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT))
|
urlpatterns.extend(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
p {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,7 @@
|
||||||
const fileInput = document.getElementById('file-upload');
|
const fileInput = document.getElementById('file-upload');
|
||||||
const upload_ready = false;
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
fileInput.addEventListener('change', handleFileChange);
|
let file_id;
|
||||||
|
|
||||||
function handleFileChange(event) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
const file_size = file.size;
|
|
||||||
|
|
||||||
console.log("Max file bytes is : ", max_file_bytes);
|
|
||||||
console.log("File size is: ", file_size);
|
|
||||||
|
|
||||||
if (file_size > max_file_bytes){
|
|
||||||
console.log("PLACEHOLDER: Size too big man.");
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("PLACEHOLDER: Ready!");
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileUpload(event) {
|
function handleFileUpload(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from storage.models import File
|
|
||||||
|
|
||||||
|
|
||||||
class FileForm(forms.ModelForm):
|
|
||||||
|
|
||||||
set_name = forms.BooleanField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = File
|
|
||||||
exclude = File.readonly_fields
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from common.constants import UPLOAD_STATUS_TYPES
|
from common.constants import CONFIG_KEYS, UPLOAD_STATUS_TYPES
|
||||||
from common.models import LockboxBase
|
from common.models import LockboxBase
|
||||||
from common.utils import get_config, get_max_size_chunk_bytes
|
from common.utils import get_config, get_max_size_chunk_bytes, normalize_string
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.uploadedfile import UploadedFile
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
@ -26,7 +26,7 @@ class File(LockboxBase):
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
verbose_name = _("name"),
|
verbose_name = _("name"),
|
||||||
help_text=_("Name of the file"),
|
help_text=_("display name of this file"),
|
||||||
)
|
)
|
||||||
|
|
||||||
extension = models.CharField(
|
extension = models.CharField(
|
||||||
|
|
@ -75,7 +75,7 @@ class File(LockboxBase):
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="files_owned",
|
related_name="files_owned",
|
||||||
verbose_name=_("owner"),
|
verbose_name=_("owner"),
|
||||||
help_text=_("Who owns this file"),
|
help_text=_("owner of this file"),
|
||||||
)
|
)
|
||||||
|
|
||||||
expires = models.BooleanField(
|
expires = models.BooleanField(
|
||||||
|
|
@ -132,11 +132,11 @@ class File(LockboxBase):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_expires(self):
|
def date_expires(self):
|
||||||
return self.date_created + timedelta(minutes=get_config("EXPIRATION_DELTA_MINUTES"))
|
return self.date_created + timedelta(minutes=get_config("EXPIRATION_DELTA_MINUTES").value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def abandoned(self):
|
def abandoned(self):
|
||||||
return self.date_created + timedelta(minutes=get_config("ABANDONED_DELTA_MINUTES"))
|
return self.date_created + timedelta(minutes=get_config("ABANDONED_DELTA_MINUTES").value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expired(self):
|
def expired(self):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path, re_path
|
||||||
from rest_framework.routers import SimpleRouter
|
from rest_framework.routers import SimpleRouter
|
||||||
from rest_framework_nested.routers import NestedSimpleRouter
|
from rest_framework_nested.routers import NestedSimpleRouter
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ chunk_router = NestedSimpleRouter(router, r'files', lookup="file")
|
||||||
chunk_router.register(r'chunks', views_api.FileChunkViewSet, basename="file-chunks")
|
chunk_router.register(r'chunks', views_api.FileChunkViewSet, basename="file-chunks")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("api/", include(router.urls)),
|
re_path(r"api/", include(router.urls)),
|
||||||
path("api/", include(chunk_router.urls)),
|
re_path(r"api/", include(chunk_router.urls)),
|
||||||
path("upload/", views_client.FileUploadView.as_view(), name="client-fileupload"),
|
path("client/files/", views_client.FileUploadView.as_view, name="client-fileupload"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class FileModelViewSet(ModelViewSet):
|
||||||
serializer_class = FileSerializer
|
serializer_class = FileSerializer
|
||||||
|
|
||||||
@action(detail=True, methods=["post"])
|
@action(detail=True, methods=["post"])
|
||||||
def finalize(self, *args, **kwargs):
|
def finalize(self, *args, **kwargs): #noqa: ARG002
|
||||||
file = self.get_object()
|
file = self.get_object()
|
||||||
file.status = UPLOAD_STATUS_TYPES.PROCESSING
|
file.status = UPLOAD_STATUS_TYPES.PROCESSING
|
||||||
file.save()
|
file.save()
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
from common.utils import get_config
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from storage.forms import FileForm
|
|
||||||
|
|
||||||
|
|
||||||
class FileUploadView(View):
|
class FileUploadView(View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
context = {
|
context = {}
|
||||||
"form": FileForm,
|
|
||||||
"max_chunk_bytes": get_config("MAX_CHUNK_BYTES"),
|
|
||||||
"max_file_bytes": get_config("MAX_FILE_BYTES"),
|
|
||||||
}
|
|
||||||
return render(request, "storage/upload.html", context=context)
|
return render(request, "storage/upload.html", context=context)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Files</a></li>
|
<li><a href="/">Files</a></li>
|
||||||
<li><a href="{% url 'client-fileupload' %}">Upload</a></li>
|
<li><a href="/files/upload/">Upload</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,34 +3,21 @@
|
||||||
{% block title %}Upload a file{% endblock %}
|
{% block title %}Upload a file{% endblock %}
|
||||||
|
|
||||||
{% block postjs %}
|
{% block postjs %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const chunk_size = {{ max_chunk_bytes }};
|
const chunk_size = 1024 * 1024
|
||||||
const max_file_bytes = {{ max_file_bytes }};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="{% static 'js/chunked_uploader.js' %}"></script>
|
<script src="{% static 'js/chunked_uploader.js' %}"></script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<p> Upload file </p>
|
<p> Upload file </p>
|
||||||
|
|
||||||
<table>
|
|
||||||
{{form}}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p id="file-size"></p>
|
|
||||||
|
|
||||||
<form method="post">{% csrf_token %}
|
<form method="post">{% csrf_token %}
|
||||||
<input type="file" id="file-upload">
|
<input type="file" id="file-upload">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@ class TestUser:
|
||||||
Test user related functions are working correctly.
|
Test user related functions are working correctly.
|
||||||
"""
|
"""
|
||||||
def test_stub(self):
|
def test_stub(self):
|
||||||
assert False
|
assert True
|
||||||
|
|
|
||||||
|
|
@ -232,20 +232,6 @@ pytest = ">=7.0.0"
|
||||||
docs = ["sphinx", "sphinx-rtd-theme"]
|
docs = ["sphinx", "sphinx-rtd-theme"]
|
||||||
testing = ["Django", "django-configurations (>=2.0)"]
|
testing = ["Django", "django-configurations (>=2.0)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-dotenv"
|
|
||||||
version = "1.0.1"
|
|
||||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
|
||||||
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
cli = ["click (>=5.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2024.1"
|
version = "2024.1"
|
||||||
|
|
@ -327,4 +313,4 @@ brotli = ["Brotli"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "3.12"
|
python-versions = "3.12"
|
||||||
content-hash = "e338f5cc37553ef6a4799746f6feb537427330934b43caee4aa73c3b74a0fb9e"
|
content-hash = "600fd336e3593b6fc5503b70e73dd2c22a2922a1662240f540ac6399520ae6d2"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ django = "~4.2.0"
|
||||||
whitenoise = "^6.6.0"
|
whitenoise = "^6.6.0"
|
||||||
djangorestframework = "^3.14.0"
|
djangorestframework = "^3.14.0"
|
||||||
drf-nested-routers = "^0.93.5"
|
drf-nested-routers = "^0.93.5"
|
||||||
python-dotenv = "^1.0.1"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^8.0.0"
|
pytest = "^8.0.0"
|
||||||
|
|
@ -68,11 +67,8 @@ target-version = "py312"
|
||||||
select = ["ALL"]
|
select = ["ALL"]
|
||||||
ignore = [
|
ignore = [
|
||||||
"ANN",
|
"ANN",
|
||||||
"ARG001",
|
|
||||||
"ARG002",
|
|
||||||
"D",
|
"D",
|
||||||
"DJ001",
|
"DJ001",
|
||||||
"DJ006",
|
|
||||||
"DJ012",
|
"DJ012",
|
||||||
"ERA001",
|
"ERA001",
|
||||||
"FIX",
|
"FIX",
|
||||||
|
|
@ -80,12 +76,11 @@ ignore = [
|
||||||
"PLR0913",
|
"PLR0913",
|
||||||
"Q000",
|
"Q000",
|
||||||
"RUF012",
|
"RUF012",
|
||||||
"S101",
|
"TRY",
|
||||||
"SIM102",
|
|
||||||
"SLF001",
|
|
||||||
"T201",
|
"T201",
|
||||||
"TD",
|
"TD",
|
||||||
"TRY",
|
"S101",
|
||||||
|
"SLF001",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
asgiref==3.7.2 ; python_version == "3.12"
|
|
||||||
django==4.2.10 ; python_version == "3.12"
|
|
||||||
djangorestframework==3.14.0 ; python_version == "3.12"
|
|
||||||
drf-nested-routers==0.93.5 ; python_version == "3.12"
|
|
||||||
python-dotenv==1.0.1 ; python_version == "3.12"
|
|
||||||
pytz==2024.1 ; python_version == "3.12"
|
|
||||||
sqlparse==0.4.4 ; python_version == "3.12"
|
|
||||||
tzdata==2023.4 ; sys_platform == "win32" and python_version == "3.12"
|
|
||||||
whitenoise==6.6.0 ; python_version == "3.12"
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
asgiref==3.7.2 ; python_version == "3.12"
|
|
||||||
colorama==0.4.6 ; python_version == "3.12" and sys_platform == "win32"
|
|
||||||
coverage[toml]==7.4.1 ; python_version == "3.12"
|
|
||||||
django==4.2.10 ; python_version == "3.12"
|
|
||||||
djangorestframework==3.14.0 ; python_version == "3.12"
|
|
||||||
drf-nested-routers==0.93.5 ; python_version == "3.12"
|
|
||||||
iniconfig==2.0.0 ; python_version == "3.12"
|
|
||||||
packaging==23.2 ; python_version == "3.12"
|
|
||||||
pluggy==1.4.0 ; python_version == "3.12"
|
|
||||||
pytest-cov==4.1.0 ; python_version == "3.12"
|
|
||||||
pytest-django==4.8.0 ; python_version == "3.12"
|
|
||||||
pytest==8.0.0 ; python_version == "3.12"
|
|
||||||
python-dotenv==1.0.1 ; python_version == "3.12"
|
|
||||||
pytz==2024.1 ; python_version == "3.12"
|
|
||||||
ruff==0.2.1 ; python_version == "3.12"
|
|
||||||
sqlparse==0.4.4 ; python_version == "3.12"
|
|
||||||
tzdata==2023.4 ; sys_platform == "win32" and python_version == "3.12"
|
|
||||||
whitenoise==6.6.0 ; python_version == "3.12"
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
cd lockbox
|
|
||||||
pytest --cov=. --cov-report term-missing
|
|
||||||
Loading…
Reference in New Issue