Added very basic storage model and WIP API
This commit is contained in:
parent
3f89efcb36
commit
ead978715c
4
.flake8
4
.flake8
|
|
@ -1,4 +0,0 @@
|
||||||
[flake8]
|
|
||||||
max-line-length=120
|
|
||||||
ignore = E126,E128,W504
|
|
||||||
exclude = */migrations/[0-9]*, .pyscripts/*,pyenv*,.pyenv*, .git, .venv
|
|
||||||
|
|
@ -47,6 +47,7 @@ coverage.xml
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
cover/
|
cover/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
|
@ -89,4 +90,6 @@ ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
lockbox/media
|
||||||
5
Makefile
5
Makefile
|
|
@ -1,11 +1,8 @@
|
||||||
lint:
|
lint:
|
||||||
@flake8 --config=.flake8 /dev/null $(shell git diff --name-only HEAD | grep '\.py$$' )
|
@ruff check $(shell git diff --diff-filter=ACM --name-only HEAD | grep '\.py$$' )
|
||||||
|
|
||||||
stampreqs:
|
stampreqs:
|
||||||
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
|
|
||||||
test:
|
test:
|
||||||
pytest --cov=. --cov-report term-missing
|
pytest --cov=. --cov-report term-missing
|
||||||
|
|
||||||
testcap:
|
|
||||||
pytest --cov=. --cov-report term-missing -s
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
# Register your models here.
|
from common.models import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
class LockboxModelAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = Configuration.readonly_fields
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Configuration, LockboxModelAdmin)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import (
|
||||||
|
AppConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommonConfig(AppConfig):
|
class CommonConfig(AppConfig):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
class UPLOAD_STATUS_TYPES:
|
||||||
|
UPLOADING = "uploading"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
ABANDONED = "abandoned"
|
||||||
|
|
||||||
|
|
||||||
|
# Config
|
||||||
|
|
||||||
|
CONFIG_KEYS = {
|
||||||
|
"EXPIRATION_DELTA_MINUTES": {
|
||||||
|
"description": "Date created + this delta at which file expires",
|
||||||
|
"verbose_name": "File expiration delta (minutes)",
|
||||||
|
"native_type": int,
|
||||||
|
"default": 120,
|
||||||
|
},
|
||||||
|
|
||||||
|
"ABANDONED_DELTA_MINUTES": {
|
||||||
|
"description": "Date created + this delta at which a file is marked as abandoned",
|
||||||
|
"verbose_name": "Uncompleted file abandoned max age",
|
||||||
|
"native_type": int,
|
||||||
|
"default": 20,
|
||||||
|
},
|
||||||
|
|
||||||
|
"ABANDONED_EXPIRED_SCAN_INTERVAL": {
|
||||||
|
"description": "Scan and scrub abandoned or expired uploads",
|
||||||
|
"verbose_name": "Scan interval for abandoned/expired files",
|
||||||
|
"native_type": int,
|
||||||
|
"default": 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-02-12 08:39
|
||||||
|
|
||||||
|
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')], help_text='internal configuration key name', max_length=50)),
|
||||||
|
('value', models.CharField(help_text='actual DB config value', max_length=1024)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,24 +1,41 @@
|
||||||
from django.db import models
|
from typing import Any, NamedTuple
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
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(
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
default=uuid4
|
default=uuid4,
|
||||||
|
verbose_name=_("lockbox ID"),
|
||||||
)
|
)
|
||||||
|
|
||||||
date_created = models.DateTimeField(
|
date_created = models.DateTimeField(
|
||||||
verbose_name=_("date created")
|
verbose_name=_("date created"),
|
||||||
|
help_text=_("date at which this object was created"),
|
||||||
|
null=False,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
date_updated = models.DateTimeField(
|
date_updated = models.DateTimeField(
|
||||||
verbose_name=_("date updated"),
|
verbose_name=_("date updated"),
|
||||||
null=True
|
help_text=_("date at which this object was last updated"),
|
||||||
|
null=False,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readonly_fields = [
|
||||||
|
"date_created",
|
||||||
|
"date_updated",
|
||||||
|
"lid",
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
@ -29,6 +46,47 @@ class LockboxBase(models.Model): # pragma: no cover
|
||||||
if not self.date_created:
|
if not self.date_created:
|
||||||
self.date_created = now
|
self.date_created = now
|
||||||
|
|
||||||
self.date_update = now
|
self.date_updated = now
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.__class__.__name__} Object {self.lid}"
|
||||||
|
|
||||||
|
|
||||||
|
class Config(NamedTuple):
|
||||||
|
key: str
|
||||||
|
value: Any
|
||||||
|
native_type: type # change to type
|
||||||
|
description: str
|
||||||
|
source: str
|
||||||
|
default: Any
|
||||||
|
|
||||||
|
|
||||||
|
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,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
from os import getenv
|
||||||
|
from unicodedata import normalize
|
||||||
|
|
||||||
|
from common.constants import CONFIG_KEYS
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_string(string, form="NFKC"):
|
||||||
|
return normalize(form, string)
|
||||||
|
|
||||||
|
def cast_to_native_type(key, value, native_type):
|
||||||
|
try:
|
||||||
|
return native_type(value)
|
||||||
|
except ValueError as e:
|
||||||
|
message = (
|
||||||
|
f"Received unexpected value type for configuration key {key}\nValue: {value}\nExpected type : {native_type}"
|
||||||
|
)
|
||||||
|
raise ValueError(message) from e
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(key):
|
||||||
|
from common.models import Config, Configuration
|
||||||
|
config = Config(CONFIG_KEYS[key])
|
||||||
|
|
||||||
|
obj = Configuration.objects.filter(key=key).first()
|
||||||
|
|
||||||
|
if obj:
|
||||||
|
config.value = cast_to_native_type(key, obj.value, config.native_type)
|
||||||
|
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
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
from django.shortcuts import render
|
# from django.shortcuts import render
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
"""
|
"""Lockbox File Sharing"""
|
||||||
Lockbox File Sharing
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# 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 = os.getenv("LOCKBOX_SECRET_KEY")
|
||||||
DEBUG = os.getenv("LOCKBOX_DEBUG", False)
|
DEBUG = os.getenv("LOCKBOX_DEBUG")
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
@ -23,12 +21,13 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
# Vendors
|
# Vendors
|
||||||
|
|
||||||
# Apps
|
# Apps
|
||||||
'common',
|
'common',
|
||||||
'user',
|
'user',
|
||||||
|
'storage',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
@ -67,7 +66,7 @@ DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -101,6 +100,10 @@ USE_TZ = True
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
MEDIA_ROOT = BASE_DIR / "media"
|
||||||
|
MEDIA_URL = "files/"
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
AUTH_USER_MODEL = 'user.LockboxUser'
|
AUTH_USER_MODEL = 'user.LockboxUser'
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,13 @@ Including another URLconf
|
||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
urlpatterns.extend(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from storage.models import File
|
||||||
|
|
||||||
|
|
||||||
|
class FileAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = File.readonly_fields
|
||||||
|
|
||||||
|
admin.site.register(File, FileAdmin)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class StorageConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'storage'
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-02-12 09:59
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import storage.models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='File',
|
||||||
|
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')),
|
||||||
|
('name', models.CharField(help_text='display name of this file', max_length=255, verbose_name='name')),
|
||||||
|
('extension', models.CharField(blank=True, help_text='reported filesystem extension (not mime type)', max_length=128, null=True, verbose_name='extension')),
|
||||||
|
('file', models.FileField(help_text='actual file', upload_to=storage.models.get_upload_path, verbose_name='file')),
|
||||||
|
('position', models.PositiveBigIntegerField(default=0, help_text='current position of uploaded bytes', verbose_name='position')),
|
||||||
|
('status', models.CharField(choices=[('uploading', 'uploading'), ('completed', 'completed'), ('abandoned', 'abandoned')], default='uploading', help_text='upload status for file', max_length=9, verbose_name='status')),
|
||||||
|
('date_completed', models.DateTimeField(blank=True, help_text="datetime at which this file's upload was completed", null=True, verbose_name='completed on')),
|
||||||
|
('expires', models.BooleanField(default=False, help_text="will be scrubbed on 'date_expires'", verbose_name='expires')),
|
||||||
|
('delete_on_expiration', models.BooleanField(default=False, help_text='will be deleted if expired and expires is true', verbose_name='delete on expiration')),
|
||||||
|
('size_on_disk', models.PositiveBigIntegerField(blank=True, help_text='total size on disk for this file', null=True, verbose_name='size on disk (bytes)')),
|
||||||
|
('owner', models.ForeignKey(blank=True, help_text='owner of this file', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='files_owned', to=settings.AUTH_USER_MODEL, verbose_name='owner')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'file',
|
||||||
|
'verbose_name_plural': 'files',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from common.constants import CONFIG_KEYS, UPLOAD_STATUS_TYPES
|
||||||
|
from common.models import LockboxBase
|
||||||
|
from common.utils import get_config, normalize_string
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def get_upload_path(instance, filename):
|
||||||
|
filename = normalize_string(filename) + ".part"
|
||||||
|
file_subdir = settings.MEDIA_ROOT / str(instance.lid)
|
||||||
|
|
||||||
|
if not Path.exists(file_subdir):
|
||||||
|
Path.mkdir(file_subdir)
|
||||||
|
|
||||||
|
return Path(str(instance.lid)) / Path(filename)
|
||||||
|
|
||||||
|
|
||||||
|
class File(LockboxBase):
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name = _("name"),
|
||||||
|
help_text=_("display name of this file"),
|
||||||
|
)
|
||||||
|
|
||||||
|
extension = models.CharField(
|
||||||
|
max_length=128,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("extension"),
|
||||||
|
help_text=_("reported filesystem extension (not mime type)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
file = models.FileField(
|
||||||
|
upload_to=get_upload_path,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("file"),
|
||||||
|
help_text=_("actual file"),
|
||||||
|
)
|
||||||
|
|
||||||
|
position = models.PositiveBigIntegerField(
|
||||||
|
default=0,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
verbose_name=_("position"),
|
||||||
|
help_text=_("current position of uploaded bytes"),
|
||||||
|
)
|
||||||
|
|
||||||
|
UPLOAD_CHOICES = (
|
||||||
|
(UPLOAD_STATUS_TYPES.UPLOADING, _(UPLOAD_STATUS_TYPES.UPLOADING)),
|
||||||
|
(UPLOAD_STATUS_TYPES.COMPLETED, _(UPLOAD_STATUS_TYPES.COMPLETED)),
|
||||||
|
(UPLOAD_STATUS_TYPES.ABANDONED, _(UPLOAD_STATUS_TYPES.ABANDONED)),
|
||||||
|
)
|
||||||
|
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=9,
|
||||||
|
choices=UPLOAD_CHOICES,
|
||||||
|
default=UPLOAD_STATUS_TYPES.UPLOADING,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
verbose_name=_("status"),
|
||||||
|
help_text=_("upload status for file"),
|
||||||
|
)
|
||||||
|
|
||||||
|
date_completed = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("completed on"),
|
||||||
|
help_text=_("datetime at which this file's upload was completed"),
|
||||||
|
)
|
||||||
|
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
"user.LockboxUser",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="files_owned",
|
||||||
|
verbose_name=_("owner"),
|
||||||
|
help_text=_("owner of this file"),
|
||||||
|
)
|
||||||
|
|
||||||
|
expires = models.BooleanField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default=False,
|
||||||
|
verbose_name = _("expires"),
|
||||||
|
help_text=_("will be scrubbed on 'date_expires'"),
|
||||||
|
)
|
||||||
|
|
||||||
|
delete_on_expiration = models.BooleanField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("delete on expiration"),
|
||||||
|
help_text=_("will be deleted if expired and expires is true"),
|
||||||
|
)
|
||||||
|
|
||||||
|
size_on_disk = models.PositiveBigIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("size on disk (bytes)"),
|
||||||
|
help_text=_("total size on disk for this file"),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = [
|
||||||
|
"extension",
|
||||||
|
"position",
|
||||||
|
"status",
|
||||||
|
"date_completed",
|
||||||
|
"size_on_disk",
|
||||||
|
*LockboxBase.readonly_fields,
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("file")
|
||||||
|
verbose_name_plural = _("files")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def md5(self):
|
||||||
|
if getattr(self, "_md5", None) is None:
|
||||||
|
md5 = hashlib.md5() # noqa:S324 this is needed due to how chunked files work.
|
||||||
|
for chunk in self.file.chunks():
|
||||||
|
md5.update(chunk)
|
||||||
|
self._md5 = md5.hexdigest()
|
||||||
|
return self._md5
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_expires(self):
|
||||||
|
return self.date_created + get_config(CONFIG_KEYS["EXPIRATION_DELTA_MINUTES"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abandoned(self):
|
||||||
|
return self.date_created + get_config(CONFIG_KEYS["ABANDONED_DELTA_MINUTES"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
return self.date_expires <= timezone.now()
|
||||||
|
|
||||||
|
def delete(self, *args, delete_file=True, **kwargs):
|
||||||
|
if self.file:
|
||||||
|
storage, path = self.file.storage, self.file.path
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
if self.file and delete_file:
|
||||||
|
storage.delete(path)
|
||||||
|
|
||||||
|
def get_file_handler_bytes(self):
|
||||||
|
self.file.close()
|
||||||
|
self.file.open(mode="rb")
|
||||||
|
return UploadedFile(file=self.file, name=self.filename, size=self.offset)
|
||||||
|
|
||||||
|
def append_chunk(self, chunk, chunk_size=None, save=None):
|
||||||
|
# file handler might be open for some bizzare reason
|
||||||
|
self.file.close()
|
||||||
|
|
||||||
|
with Path.open(self.file.path, mode="ab") as fh:
|
||||||
|
fh.write(
|
||||||
|
chunk.read(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if chunk_size is not None:
|
||||||
|
# file is chunked
|
||||||
|
self.postition += chunk_size
|
||||||
|
elif hasattr(chunk, "size"):
|
||||||
|
self.postition += chunk.size
|
||||||
|
else:
|
||||||
|
# file is one shot (small file)
|
||||||
|
self.postition = self.file.size
|
||||||
|
self._md5 = None
|
||||||
|
if save:
|
||||||
|
self.save()
|
||||||
|
self.file.close()
|
||||||
|
|
||||||
|
|
||||||
|
# class FileShare(LockboxBase):
|
||||||
|
# file = models.ForeignKey(
|
||||||
|
# "storage.File",
|
||||||
|
# null=False,
|
||||||
|
# blank=False,
|
||||||
|
# on_delete=models.CASCADE,
|
||||||
|
# related_name="shares",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def __str__(self):
|
||||||
|
# return self.file.name
|
||||||
|
|
||||||
|
# class Meta:
|
||||||
|
# verbose_name = _("share")
|
||||||
|
# verbose_name_plural = _("shares")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
# Register your models here.
|
from user.models import LockboxUser
|
||||||
|
|
||||||
|
|
||||||
|
class LockboxUserAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = LockboxUser.readonly_fields
|
||||||
|
|
||||||
|
admin.site.register(LockboxUser, LockboxUserAdmin)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib.auth.base_user import BaseUserManager
|
from django.contrib.auth.base_user import BaseUserManager
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
class LockboxUserManager(BaseUserManager):
|
class LockboxUserManager(BaseUserManager):
|
||||||
def create_user(self, username, password, **extra_fields):
|
def create_user(self, username, password, **extra_fields):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 4.2.10 on 2024-02-10 10:51
|
# Generated by Django 4.2.10 on 2024-02-12 08:39
|
||||||
|
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
@ -25,10 +25,10 @@ class Migration(migrations.Migration):
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
('lid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
('lid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='lockbox ID')),
|
||||||
('date_created', models.DateTimeField(verbose_name='date created')),
|
('date_created', models.DateTimeField(blank=True, help_text='date at which this object was created', verbose_name='date created')),
|
||||||
('date_updated', models.DateTimeField(null=True, verbose_name='date updated')),
|
('date_updated', models.DateTimeField(blank=True, help_text='date at which this object was last updated', verbose_name='date updated')),
|
||||||
('alias', models.SlugField(blank=True, help_text='An alias or nickname to remember who this is', max_length=32, null=True, unique=True, verbose_name='name')),
|
('alias', models.SlugField(blank=True, help_text='an alias or nickname to remember who this is', max_length=32, null=True, unique=True, verbose_name='name')),
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
|
from common.models import LockboxBase
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from common.models import LockboxBase
|
|
||||||
from user.managers import LockboxUserManager
|
from user.managers import LockboxUserManager
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,8 +13,7 @@ class LockboxUser(AbstractUser, LockboxBase):
|
||||||
unique=True,
|
unique=True,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
validators=[],
|
help_text=_("an alias or nickname to remember who this is"),
|
||||||
help_text=_("An alias or nickname to remember who this is")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Void this stuff.
|
# Void this stuff.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
from user.models import LockboxUser
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db()
|
@pytest.mark.django_db()
|
||||||
class TestUser:
|
class TestUser:
|
||||||
"""
|
"""
|
||||||
Test util default creation functions are working.
|
Test user related functions are working correctly.
|
||||||
"""
|
"""
|
||||||
|
def test_stub(self):
|
||||||
|
assert True
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
from django.shortcuts import render
|
# from django.shortcuts import render
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,32 @@ 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 = "ruff"
|
||||||
|
version = "0.2.1"
|
||||||
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
|
||||||
|
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
|
||||||
|
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
|
@ -232,4 +258,4 @@ files = [
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "3.12"
|
python-versions = "3.12"
|
||||||
content-hash = "824df996658026864e02f217b5df60d87723b3f06f3e8c5c1d265abf7851b978"
|
content-hash = "1a0f3ef0953e311eaf5daaff9b7980e0211fb0e5195344a28881b731bca3b7eb"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ django = "~4.2.0"
|
||||||
pytest = "^8.0.0"
|
pytest = "^8.0.0"
|
||||||
pytest-django = "^4.8.0"
|
pytest-django = "^4.8.0"
|
||||||
pytest-cov = "^4.1.0"
|
pytest-cov = "^4.1.0"
|
||||||
|
ruff = "^0.2.1"
|
||||||
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|
@ -46,6 +47,34 @@ omit = [
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [
|
||||||
|
"*/migrations/[0-9]*",
|
||||||
|
".pyscripts/*",
|
||||||
|
"pyenv*",
|
||||||
|
".pyenv*",
|
||||||
|
".git",
|
||||||
|
".venv",
|
||||||
|
]
|
||||||
|
|
||||||
|
line-length = 120
|
||||||
|
target-version = "py312"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["ALL"]
|
||||||
|
ignore = [
|
||||||
|
"ANN",
|
||||||
|
"D",
|
||||||
|
"DJ001",
|
||||||
|
"DJ012",
|
||||||
|
"ERA001",
|
||||||
|
"N801",
|
||||||
|
"Q000",
|
||||||
|
"RUF012",
|
||||||
|
"TRY",
|
||||||
|
"S101",
|
||||||
|
"SLF001",
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue