phase 1 backup - support for multi-file versioned upload

This commit is contained in:
JakeBreath
2026-08-03 22:46:22 -05:00
parent 5e58b8a183
commit f09acedbcf
14 changed files with 375 additions and 91 deletions
+92 -42
View File
@@ -25,6 +25,7 @@ from .models import (
TagList,
TempUpload,
Version,
VersionFile,
slugify_tag,
)
from .storage import delete_file_index, move_file_index, store_temp_file
@@ -307,7 +308,7 @@ def project_detail(request, slug):
project = get_object_or_404(
Project.objects.select_related('owner', 'thumbnail'), slug=slug,
)
versions = project.versions.select_related('file').all()
versions = project.versions.prefetch_related('files__file').all()
assets = project.assets.select_related('file').all()
tags = project.tag_links.select_related('tag__category').all()
can_edit = project.can_edit(request.user)
@@ -323,7 +324,7 @@ def project_detail(request, slug):
DRAFT_KEYS = (
'title', 'summary', 'category', 'description',
'tags', 'caption', 'version_name', 'changelog',
'tags', 'version_name', 'changelog',
)
@@ -344,6 +345,13 @@ def _pending_uploads(user):
)
def _pending_kind(user, kind):
return list(
TempUpload.objects.filter(user=user, status='pending', kind=kind)
.select_related('file').order_by('created_at')
)
def _latest_pending(user, kind):
return (
TempUpload.objects.filter(user=user, status='pending', kind=kind)
@@ -358,6 +366,35 @@ def _adopt_temp(upload, project_pk, subdir, kind):
return move_file_index(upload.file, new_path, kind=kind)
def _classify_version_file(file_index, category):
"""Pick a VersionFile.kind from the uploaded file's extension."""
name = (file_index.original_filename or '').lower()
if name.endswith('.md'):
return 'markdown'
if name.endswith('.png'):
return 'png'
return 'release'
def _finalize_version(project, version_name, changelog, temp_uploads):
"""Adopt pending 'version' temp uploads into a new Version as VersionFiles."""
version = Version.objects.create(
project=project,
version_name=version_name,
changelog=changelog,
)
for temp in temp_uploads:
index = _adopt_temp(temp, project.pk, 'versions', 'version')
VersionFile.objects.create(
version=version,
file=index,
kind=_classify_version_file(index, project.category),
)
temp.status = 'used'
temp.save(update_fields=['status'])
return version
def _form_values(*forms):
out = {}
for form in forms:
@@ -376,9 +413,9 @@ def project_create(request):
version_form = VersionForm(request.POST)
if project_form.is_valid() and version_form.is_valid():
data = project_form.cleaned_data
version_file = _latest_pending(request.user, 'version')
if version_file is None:
version_form.add_error('version_name', 'Upload the version file before creating the project.')
version_files = _pending_kind(request.user, 'version')
if not version_files:
version_form.add_error('version_name', 'Upload at least one version file before creating the project.')
else:
project = Project.objects.create(
slug=_unique_project_slug(data['title']),
@@ -396,24 +433,21 @@ def project_create(request):
thumb.save(update_fields=['status'])
project.save(update_fields=['thumbnail'])
version_index = _adopt_temp(version_file, project.pk, 'versions', 'version')
version_file.status = 'used'
version_file.save(update_fields=['status'])
Version.objects.create(
project=project,
version_name=version_form.cleaned_data['version_name'],
file=version_index,
changelog=version_form.cleaned_data['changelog'],
_finalize_version(
project,
version_form.cleaned_data['version_name'],
version_form.cleaned_data['changelog'],
version_files,
)
caption = data.get('caption', '')
for temp in TempUpload.objects.filter(
user=request.user, status='pending', kind='media',
).select_related('file').order_by('created_at'):
index = _adopt_temp(temp, project.pk, 'gallery', 'asset')
ProjectAsset.objects.create(
project=project, file=index,
caption=caption, uploaded_by=request.user,
caption=request.POST.get(f'caption_{temp.uuid}', '')[:128],
uploaded_by=request.user,
)
temp.status = 'used'
temp.save(update_fields=['status'])
@@ -511,7 +545,8 @@ def project_delete(request, slug):
indices = []
if project.thumbnail_id:
indices.append(project.thumbnail)
indices += [v.file for v in project.versions.all() if v.file_id]
for version in project.versions.prefetch_related('files__file').all():
indices += [vf.file for vf in version.files.all() if vf.file_id]
indices += [a.file for a in project.assets.all() if a.file_id]
project.delete()
for index in indices:
@@ -531,41 +566,56 @@ def version_upload(request, slug):
if request.method == 'POST':
form = VersionForm(request.POST)
if form.is_valid():
version_file = _latest_pending(request.user, 'version')
if version_file is None:
form.add_error('version_name', 'Upload the version file before uploading.')
version_files = _pending_kind(request.user, 'version')
if not version_files:
form.add_error('version_name', 'Upload at least one version file before uploading.')
else:
index = _adopt_temp(version_file, project.pk, 'versions', 'version')
version_file.status = 'used'
version_file.save(update_fields=['status'])
Version.objects.create(
project=project,
version_name=form.cleaned_data['version_name'],
file=index,
changelog=form.cleaned_data['changelog'],
_finalize_version(
project,
form.cleaned_data['version_name'],
form.cleaned_data['changelog'],
version_files,
)
messages.success(request, f'Version {form.cleaned_data["version_name"]} uploaded.')
return redirect('library:project_detail', slug=project.slug)
version_file = _latest_pending(request.user, 'version')
version_files = _pending_kind(request.user, 'version')
return render(request, 'library/version_upload.html', {
'project': project,
'version_form': form,
'pending_uploads': [_serialize_temp(version_file)] if version_file else [],
'pending_uploads': [_serialize_temp(u) for u in version_files],
})
def _bump_and_redirect(request, version, file_index):
Version.objects.filter(pk=version.pk).update(downloads=F('downloads') + 1)
return redirect(
reverse('library:file_request', args=[file_index.uuid]) + '?download=1'
)
def version_download(request, slug, version_id):
"""Download the first release file of a version (kept for API/mods)."""
version = get_object_or_404(
Version.objects.select_related('file', 'project'),
Version.objects.select_related('project'),
pk=version_id, project__slug=slug,
)
if version.file_id:
Version.objects.filter(pk=version.pk).update(downloads=F('downloads') + 1)
return redirect(
reverse('library:file_request', args=[version.file.uuid]) + '?download=1'
)
raise Http404
first = version.files.select_related('file').first()
if first is None:
raise Http404
return _bump_and_redirect(request, version, first.file)
def version_file_download(request, slug, version_id, file_uuid):
"""Download a specific file of a version."""
version = get_object_or_404(
Version.objects.select_related('project'),
pk=version_id, project__slug=slug,
)
vf = get_object_or_404(
version.files.select_related('file'), file__uuid=file_uuid,
)
return _bump_and_redirect(request, version, vf.file)
@login_required
@@ -575,7 +625,6 @@ def asset_upload(request, slug):
return HttpResponseForbidden('You do not have permission to add media.')
if request.method == 'POST':
caption = request.POST.get('caption', '')
media = TempUpload.objects.filter(user=request.user, status='pending', kind='media')
if not media.exists():
messages.error(request, 'No media uploaded yet.')
@@ -585,7 +634,8 @@ def asset_upload(request, slug):
index = _adopt_temp(temp, project.pk, 'gallery', 'asset')
ProjectAsset.objects.create(
project=project, file=index,
caption=caption, uploaded_by=request.user,
caption=request.POST.get(f'caption_{temp.uuid}', '')[:128],
uploaded_by=request.user,
)
temp.status = 'used'
temp.save(update_fields=['status'])
@@ -595,7 +645,6 @@ def asset_upload(request, slug):
return render(request, 'library/asset_upload.html', {
'project': project,
'caption': request.POST.get('caption', ''),
'pending_uploads': [_serialize_temp(u) for u in _pending_uploads(request.user)],
})
@@ -707,9 +756,10 @@ def api_upload_temp(request):
if kind == 'thumbnail' and not content_type.startswith('image/'):
return JsonResponse({'error': 'The thumbnail must be an image.'}, status=400)
# Thumbnail and version are single slots: discard any earlier pending one.
if kind in ('thumbnail', 'version'):
for old in TempUpload.objects.filter(user=request.user, status='pending', kind=kind):
# Thumbnail is a single slot: discard any earlier pending one. Version and
# media accept multiple files (one Version can hold several files).
if kind == 'thumbnail':
for old in TempUpload.objects.filter(user=request.user, status='pending', kind='thumbnail'):
delete_file_index(old.file)
old.delete()