stash
This commit is contained in:
parent
82a27ce2a8
commit
3b494dfa37
8 changed files with 112 additions and 10 deletions
|
|
@ -130,7 +130,8 @@ def getUserInfo(request):
|
|||
return Response({'profile_picture_id': 'File does not exist.'}, status=400)
|
||||
user.save()
|
||||
|
||||
if old_file and old_file != user.profile_picture and old_file.connected_items.count() == 0 and old_file.profile_picture_users.count() == 0:
|
||||
if old_file and old_file != user.profile_picture and old_file.connected_items.count() == 0 \
|
||||
and old_file.profile_picture_users.count() == 0 and old_file.staged_by_workflows.count() == 0:
|
||||
old_file.delete()
|
||||
|
||||
return Response({
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ def media_urls(request, hash_path):
|
|||
try:
|
||||
file = File.objects.filter(
|
||||
Q(connected_items__owner__in=request.user.friends_or_self()) |
|
||||
Q(profile_picture_users__in=request.user.friends_or_self())
|
||||
Q(profile_picture_users__in=request.user.friends_or_self()) |
|
||||
Q(staged_by_workflows__owner__in=request.user.user.all())
|
||||
).distinct().get(
|
||||
file=hash_path)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from rest_framework.response import Response
|
|||
from authentication.signature_auth import SignatureAuthenticationLocal
|
||||
from files.models import File
|
||||
from files.serializers import FileSerializer
|
||||
from toolshed.models import InventoryItem
|
||||
from toolshed.models import InventoryItem, WorkflowInstance
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
|
|
@ -30,6 +30,16 @@ def get_item_files(request, item_id):
|
|||
def post_item_file(request, item_id):
|
||||
try:
|
||||
item = InventoryItem.objects.get(id=item_id, owner=request.user)
|
||||
if 'file_hash' in request.data:
|
||||
# Attach a file the caller already staged on one of their own workflows, identified
|
||||
# by its content hash (which the client already computed before ever uploading it),
|
||||
# instead of re-uploading bytes that are already stored server-side.
|
||||
try:
|
||||
file = File.objects.get(hash=request.data['file_hash'], staged_by_workflows__owner=request.user)
|
||||
except File.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
item.files.add(file)
|
||||
return Response(FileSerializer(file).data, status=status.HTTP_201_CREATED)
|
||||
serializer = FileSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
file = serializer.save()
|
||||
|
|
@ -40,6 +50,31 @@ def post_item_file(request, item_id):
|
|||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
def get_staged_files(request, workflow_id):
|
||||
try:
|
||||
workflow = WorkflowInstance.objects.get(id=workflow_id, owner=request.user)
|
||||
# Staged files are private working state the client already holds in full (name, size,
|
||||
# mime_type, base64 data) from the moment it read them off disk/camera - the only thing
|
||||
# it can't already know is whether/under what hash the upload was persisted, so that's
|
||||
# all this returns, unlike the fuller FileSerializer representation item_files uses.
|
||||
return Response(list(workflow.staged_files.values_list('hash', flat=True)))
|
||||
except WorkflowInstance.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
def post_staged_file(request, workflow_id):
|
||||
try:
|
||||
workflow = WorkflowInstance.objects.get(id=workflow_id, owner=request.user)
|
||||
serializer = FileSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
file = serializer.save()
|
||||
workflow.staged_files.add(file)
|
||||
return Response({'hash': file.hash}, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except WorkflowInstance.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@api_view(['POST', 'GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthenticationLocal])
|
||||
|
|
@ -58,7 +93,8 @@ def delete_item_file(request, item_id, file_id, format=None): # /item_files/
|
|||
item = InventoryItem.objects.get(id=item_id, owner=request.user)
|
||||
file = item.files.get(id=file_id)
|
||||
item.files.remove(file_id)
|
||||
if file.connected_items.count() == 0:
|
||||
if file.connected_items.count() == 0 and file.profile_picture_users.count() == 0 \
|
||||
and file.staged_by_workflows.count() == 0:
|
||||
file.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except InventoryItem.DoesNotExist:
|
||||
|
|
@ -67,8 +103,38 @@ def delete_item_file(request, item_id, file_id, format=None): # /item_files/
|
|||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@api_view(['POST', 'GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthenticationLocal])
|
||||
def staged_files(request, workflow_id, format=None): # /staged_files/
|
||||
if request.method == 'GET':
|
||||
return get_staged_files(request, workflow_id)
|
||||
elif request.method == 'POST':
|
||||
return post_staged_file(request, workflow_id)
|
||||
|
||||
|
||||
@api_view(['DELETE'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthenticationLocal])
|
||||
def delete_staged_file(request, workflow_id, file_hash, format=None): # /staged_files/
|
||||
try:
|
||||
workflow = WorkflowInstance.objects.get(id=workflow_id, owner=request.user)
|
||||
file = workflow.staged_files.get(hash=file_hash)
|
||||
workflow.staged_files.remove(file)
|
||||
if file.connected_items.count() == 0 and file.profile_picture_users.count() == 0 \
|
||||
and file.staged_by_workflows.count() == 0:
|
||||
file.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except WorkflowInstance.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
except File.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('files/', list_all_files),
|
||||
path('item_files/<int:item_id>/', item_files),
|
||||
path('item_files/<int:item_id>/<int:file_id>/', delete_item_file),
|
||||
path('staged_files/<int:workflow_id>/', staged_files),
|
||||
path('staged_files/<int:workflow_id>/<str:file_hash>/', delete_staged_file),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from rest_framework.response import Response
|
|||
|
||||
from authentication.models import ToolshedUser, KnownIdentity
|
||||
from authentication.signature_auth import SignatureAuthentication
|
||||
from files.models import File
|
||||
from toolshed.models import InventoryItem, StorageLocation, WorkflowInstance
|
||||
from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer, WorkflowInstanceSerializer
|
||||
|
||||
|
|
@ -120,7 +121,12 @@ class WorkflowInstanceViewSet(viewsets.ModelViewSet):
|
|||
|
||||
def perform_destroy(self, instance):
|
||||
if instance.owner == self.request.user.user.get():
|
||||
staged_file_ids = list(instance.staged_files.values_list('id', flat=True))
|
||||
instance.delete()
|
||||
for file in File.objects.filter(id__in=staged_file_ids):
|
||||
if file.connected_items.count() == 0 and file.profile_picture_users.count() == 0 \
|
||||
and file.staged_by_workflows.count() == 0:
|
||||
file.delete()
|
||||
|
||||
|
||||
router.register(r'inventory_items', InventoryItemViewSet, basename='inventory_items')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.2.2
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('files', '0001_initial'),
|
||||
('toolshed', '0010_rename_name_workflowinstance_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='workflowinstance',
|
||||
name='staged_files',
|
||||
field=models.ManyToManyField(blank=True, related_name='staged_by_workflows', to='files.file'),
|
||||
),
|
||||
]
|
||||
|
|
@ -139,10 +139,11 @@ class WorkflowInstance(models.Model):
|
|||
state = models.CharField(max_length=255)
|
||||
payload = models.TextField(default='', blank=True) # an opaque, frontend-serialized JSON string on the backend.
|
||||
owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='workflows')
|
||||
staged_files = models.ManyToManyField(File, related_name='staged_by_workflows', blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.state})"
|
||||
return f"{self.slug} ({self.state})"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -284,14 +284,15 @@ def delete_user_account(user):
|
|||
def _delete_orphaned_files(file_ids):
|
||||
"""Delete File rows (and their underlying blobs) in `file_ids` that are no longer referenced.
|
||||
|
||||
A File is considered orphaned once no InventoryItem and no ToolshedUser (profile picture)
|
||||
references it anymore. Returns the number of files deleted.
|
||||
A File is considered orphaned once no InventoryItem, no ToolshedUser (profile picture), and no
|
||||
WorkflowInstance (staged file) references it anymore. Returns the number of files deleted.
|
||||
"""
|
||||
from files.models import File
|
||||
|
||||
deleted = 0
|
||||
for file_obj in File.objects.filter(id__in=file_ids):
|
||||
if file_obj.connected_items.exists() or file_obj.profile_picture_users.exists():
|
||||
if file_obj.connected_items.exists() or file_obj.profile_picture_users.exists() \
|
||||
or file_obj.staged_by_workflows.exists():
|
||||
continue
|
||||
file_obj.file.delete(save=False)
|
||||
file_obj.delete()
|
||||
|
|
|
|||
|
|
@ -203,9 +203,16 @@ class InventoryItemSerializer(serializers.ModelSerializer):
|
|||
|
||||
class WorkflowInstanceSerializer(serializers.ModelSerializer):
|
||||
owner = serializers.StringRelatedField(read_only=True)
|
||||
# The client already holds the full file (name, size, mime_type, base64 data) for anything it
|
||||
# staged itself - hash is the only thing it can't already know, so that's all this exposes,
|
||||
# unlike InventoryItemSerializer.files which needs the fuller FileSerializer representation.
|
||||
staged_files = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = WorkflowInstance
|
||||
fields = ['id', 'slug', 'state', 'payload', 'owner', 'created_at', 'updated_at']
|
||||
read_only_fields = ['owner', 'created_at', 'updated_at']
|
||||
fields = ['id', 'slug', 'state', 'payload', 'owner', 'staged_files', 'created_at', 'updated_at']
|
||||
read_only_fields = ['owner', 'staged_files', 'created_at', 'updated_at']
|
||||
|
||||
def get_staged_files(self, obj):
|
||||
return list(obj.staged_files.values_list('hash', flat=True))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue