whoops
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
837b756b0d
commit
4055f7e966
|
|
@ -0,0 +1,9 @@
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: unittest
|
||||||
|
image: python
|
||||||
|
commands:
|
||||||
|
- pip install -r requirements_test.txt
|
||||||
|
- pytest --cov=. --cov-report term-missing
|
||||||
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
|
||||||
|
|
||||||
test:
|
stampreqsci:
|
||||||
pytest --cov=. --cov-report term-missing
|
poetry export --without-hashes --with dev --format=requirements.txt > requirements_test.txt
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
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,24 +16,70 @@ 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_UPLOAD_BYTES": {
|
"MAX_CHUNK_BYTES": {
|
||||||
"description": "Max bytes that can be uploaded in one go",
|
"description": "Max bytes that can be uploaded in one go, too large of a value might result in a timeout",
|
||||||
|
"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,
|
||||||
"default": 2000000, # 2 MB
|
"sensitive": False,
|
||||||
|
"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": ".",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# 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,9 +4,6 @@ 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(
|
||||||
|
|
@ -51,54 +48,3 @@ 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)
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
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,13 +1,33 @@
|
||||||
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:
|
||||||
|
|
@ -17,28 +37,21 @@ def cast_to_native_type(key, value, native_type):
|
||||||
raise ValueError(message) from e
|
raise ValueError(message) from e
|
||||||
|
|
||||||
|
|
||||||
def get_config(key):
|
def get_config(key, value_only=True): # noqa: FBT002
|
||||||
from common.models import Config, Configuration
|
default = CONFIG_KEYS[key]
|
||||||
config = Config(**CONFIG_KEYS[key], key=key)
|
from_env = getenv(key)
|
||||||
|
|
||||||
obj = Configuration.objects.filter(key=key).first()
|
if from_env:
|
||||||
|
value = cast_to_native_type(key, from_env, default["native_type"])
|
||||||
|
source = "env"
|
||||||
|
else:
|
||||||
|
value = default["default"]
|
||||||
|
source = "default"
|
||||||
|
|
||||||
if obj:
|
if value_only:
|
||||||
config.value = cast_to_native_type(key, obj.value, config.native_type)
|
return value
|
||||||
config.source = "db"
|
return Config(key=key, value=value, source=source, **default)
|
||||||
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_UPLOAD_BYTES").value
|
return get_config("MAX_CHUNK_BYTES")
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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,21 +1,23 @@
|
||||||
"""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 = os.getenv("LOCKBOX_SECRET_KEY")
|
SECRET_KEY = get_config("SECRET_KEY")
|
||||||
# DEBUG = os.getenv("LOCKBOX_DEBUG")
|
DEBUG = get_config("DEBUG")
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = get_config("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",
|
||||||
|
|
@ -64,6 +66,9 @@ 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,3 +1,4 @@
|
||||||
|
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
|
||||||
|
|
@ -6,10 +7,11 @@ 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 settings.ENABLE_BROWSABLE_API:
|
if get_config("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))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
p {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,36 @@
|
||||||
const fileInput = document.getElementById('file-upload');
|
const fileInput = document.getElementById('file-upload');
|
||||||
fileInput.addEventListener('change', handleFileUpload);
|
const upload_ready = false;
|
||||||
const csrftoken = getCookie('csrftoken');
|
|
||||||
let file_id;
|
fileInput.addEventListener('change', handleFileChange);
|
||||||
|
|
||||||
|
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];
|
||||||
let start = 0;
|
let start = 0;
|
||||||
let end = 0;
|
let end = 0;
|
||||||
let chunk;
|
let chunk;
|
||||||
|
|
||||||
while (start < file.size) {
|
while (start < file.size) {
|
||||||
chunk = file.slice(start, start + chunk_size);
|
chunk = file.slice(start, start + chunk_size);
|
||||||
end = chunk.size - start;
|
end = chunk.size - start;
|
||||||
console.log("LID: ", file_id);
|
console.log("LID: ", file_id);
|
||||||
file_id = uploadChunk(chunk, start, end, file.size, file_id);
|
file_id = uploadChunk(chunk, start, end, file.size, file_id);
|
||||||
start += chunk_size;
|
start += chunk_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadChunk(chunk, start, end, total, file_id=null) {
|
function uploadChunk(chunk, start, end, total, file_id=null) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
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 CONFIG_KEYS, UPLOAD_STATUS_TYPES
|
from common.constants import UPLOAD_STATUS_TYPES
|
||||||
from common.models import LockboxBase
|
from common.models import LockboxBase
|
||||||
from common.utils import get_config, get_max_size_chunk_bytes, normalize_string
|
from common.utils import get_config, get_max_size_chunk_bytes
|
||||||
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=_("display name of this file"),
|
help_text=_("Name of the 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=_("owner of this file"),
|
help_text=_("Who owns 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").value)
|
return self.date_created + timedelta(minutes=get_config("EXPIRATION_DELTA_MINUTES"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def abandoned(self):
|
def abandoned(self):
|
||||||
return self.date_created + timedelta(minutes=get_config("ABANDONED_DELTA_MINUTES").value)
|
return self.date_created + timedelta(minutes=get_config("ABANDONED_DELTA_MINUTES"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expired(self):
|
def expired(self):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, 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 = [
|
||||||
re_path(r"api/", include(router.urls)),
|
path("api/", include(router.urls)),
|
||||||
re_path(r"api/", include(chunk_router.urls)),
|
path("api/", include(chunk_router.urls)),
|
||||||
path("client/files/", views_client.FileUploadView.as_view, name="client-fileupload"),
|
path("upload/", 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): #noqa: ARG002
|
def finalize(self, *args, **kwargs):
|
||||||
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,8 +1,15 @@
|
||||||
|
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="/files/upload/">Upload</a></li>
|
<li><a href="{% url 'client-fileupload' %}">Upload</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,34 @@
|
||||||
{% 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 = 1024 * 1024
|
const chunk_size = {{ max_chunk_bytes }};
|
||||||
|
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 %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,20 @@ 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"
|
||||||
|
|
@ -313,4 +327,4 @@ brotli = ["Brotli"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "3.12"
|
python-versions = "3.12"
|
||||||
content-hash = "600fd336e3593b6fc5503b70e73dd2c22a2922a1662240f540ac6399520ae6d2"
|
content-hash = "e338f5cc37553ef6a4799746f6feb537427330934b43caee4aa73c3b74a0fb9e"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ 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"
|
||||||
|
|
@ -67,8 +68,11 @@ target-version = "py312"
|
||||||
select = ["ALL"]
|
select = ["ALL"]
|
||||||
ignore = [
|
ignore = [
|
||||||
"ANN",
|
"ANN",
|
||||||
|
"ARG001",
|
||||||
|
"ARG002",
|
||||||
"D",
|
"D",
|
||||||
"DJ001",
|
"DJ001",
|
||||||
|
"DJ006",
|
||||||
"DJ012",
|
"DJ012",
|
||||||
"ERA001",
|
"ERA001",
|
||||||
"FIX",
|
"FIX",
|
||||||
|
|
@ -76,11 +80,12 @@ ignore = [
|
||||||
"PLR0913",
|
"PLR0913",
|
||||||
"Q000",
|
"Q000",
|
||||||
"RUF012",
|
"RUF012",
|
||||||
"TRY",
|
"S101",
|
||||||
|
"SIM102",
|
||||||
|
"SLF001",
|
||||||
"T201",
|
"T201",
|
||||||
"TD",
|
"TD",
|
||||||
"S101",
|
"TRY",
|
||||||
"SLF001",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
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"
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
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"
|
||||||
Loading…
Reference in New Issue