From 8bd5984a94260c10351cb8e06d9389f228f9f708 Mon Sep 17 00:00:00 2001 From: Devoalda Date: Thu, 26 Oct 2023 13:39:36 +0800 Subject: [PATCH] feature(File Storage): Local File Storage Added: - Ability to do direct download - Local File Storage (Save in django media root) --- .../src/pages/downloadFile.js | 52 +++---- safeshare/safeshare/settings.py | 10 ++ safeshare/safeshare_app/views/file.py | 127 +++++++++++------- 3 files changed, 116 insertions(+), 73 deletions(-) diff --git a/safeshare/safeshare-frontend/src/pages/downloadFile.js b/safeshare/safeshare-frontend/src/pages/downloadFile.js index 1c706d0..b0e80e7 100644 --- a/safeshare/safeshare-frontend/src/pages/downloadFile.js +++ b/safeshare/safeshare-frontend/src/pages/downloadFile.js @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; +import React, {useState} from 'react'; import axios from 'axios'; -import { Link } from 'react-router-dom'; +import {Link} from 'react-router-dom'; function DownloadFile() { const [passcode, setPasscode] = useState(''); @@ -12,37 +12,42 @@ function DownloadFile() { const handleDownloadFile = () => { if (passcode) { - axios.get(`http://127.0.0.1:8000/api/files/${passcode}/`) + axios.get(`http://127.0.0.1:8000/api/files/${passcode}/`, {responseType: 'blob'}) .then(response => { - // print json data from response - console.log(response.data); - const fileData = response.data.file; - if (fileData) { - // Convert the data to a Blob - const blob = new Blob([fileData], { type: 'application/octet-stream' }); + let filename = 'downloaded_file'; // Default filename - // Create a temporary URL for the Blob - const url = window.URL.createObjectURL(blob); + // Check if the Content-Disposition header exists + if (response.headers['content-disposition']) { + const contentDisposition = response.headers['content-disposition']; + const filenameMatch = contentDisposition.match(/filename="(.+)"/); - // Create a dynamically generated anchor element - const a = document.createElement('a'); - a.href = url; - a.download = response.data.filename; - - // Trigger a click event on the anchor element to simulate a download - a.click(); - - // Clean up the temporary URL - window.URL.revokeObjectURL(url); + if (filenameMatch) { + filename = filenameMatch[1]; + } } + + const blob = new Blob([response.data], {type: 'application/octet-stream'}); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + + // Clean up the temporary URL and remove the dynamically created anchor element + window.URL.revokeObjectURL(url); + document.body.removeChild(a); }) .catch(error => { - // print error if any console.log(error); }); } }; + return (
@@ -61,7 +66,8 @@ function DownloadFile() { className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring focus:border-blue-300" />
-
diff --git a/safeshare/safeshare/settings.py b/safeshare/safeshare/settings.py index 9f3bc6f..be54820 100644 --- a/safeshare/safeshare/settings.py +++ b/safeshare/safeshare/settings.py @@ -95,6 +95,10 @@ CORS_ALLOWED_ORIGINS = [ 'http://localhost:3000', ] +CORS_EXPOSE_HEADERS = [ + 'Content-Disposition', +] + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -177,6 +181,12 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +# Media Storage +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +if not os.path.exists(MEDIA_ROOT): + os.makedirs(MEDIA_ROOT) + # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ diff --git a/safeshare/safeshare_app/views/file.py b/safeshare/safeshare_app/views/file.py index 556d05c..8530e11 100644 --- a/safeshare/safeshare_app/views/file.py +++ b/safeshare/safeshare_app/views/file.py @@ -1,13 +1,17 @@ import threading import uuid +import os from django.core.cache import cache from rest_framework.decorators import api_view from rest_framework.response import Response +from django.http import HttpResponse +from django.conf import settings +from urllib.parse import quote -@api_view(['GET', 'POST']) -def manage_items(request, *args, **kwargs): +@api_view(['POST']) +def manage_items(request): if request.method == 'POST': # Define a timeout value (in seconds) timeout = 5 @@ -15,6 +19,7 @@ def manage_items(request, *args, **kwargs): # Get the list of files and the TTL value from the request data files = request.FILES.getlist('file') ttl = request.data.get('ttl') + if not ttl: # Set ttl to 3 days ttl = 259200 # 3 * 24 * 60 * 60 @@ -28,28 +33,33 @@ def manage_items(request, *args, **kwargs): except ValueError: return Response({'msg': 'Invalid TTL format'}, status=400) - # Define a function to save a single file in a thread - def save_file_to_redis(file): + # Define a function to save a single file + def save_file_locally(file): key = uuid.uuid4().hex # Get the filename filename = file.name - # Convert file to bytes - content = file.read() + # Define the path to save the file locally + save_path = os.path.join(settings.MEDIA_ROOT, filename) - # Set with the provided TTL + # Save the file locally + with open(save_path, 'wb') as destination: + for chunk in file.chunks(): + destination.write(chunk) + + # Store the file path in the cache with the provided TTL cache.set(key, { 'filename': filename, - 'content': content + 'path': save_path, }, timeout=ttl) response = { 'key': key, 'filename': filename, - 'msg': f"{key} successfully set to {filename} with TTL {ttl} seconds" + 'msg': f"{key} successfully set to {filename} with TTL {ttl} seconds", } # Append the response to the shared responses list @@ -61,7 +71,7 @@ def manage_items(request, *args, **kwargs): # Create a thread for each file file_threads = [] for file in files: - file_thread = threading.Thread(target=save_file_to_redis, args=(file,)) + file_thread = threading.Thread(target=save_file_locally, args=(file,)) file_threads.append(file_thread) # Start all file-saving threads @@ -93,62 +103,79 @@ def manage_items(request, *args, **kwargs): @api_view(['GET', 'PUT', 'DELETE']) def manage_item(request, *args, **kwargs): if request.method == 'GET': - if kwargs['key']: + if 'key' in kwargs: value = cache.get(kwargs['key']) if value: - response = { - 'key': kwargs['key'], - 'file': value['content'], - 'filename': value['filename'], - 'msg': 'success' - } - return Response(response, status=200) + # Check if the 'path' key is in the stored value + if 'path' in value: + file_path = value['path'] + if os.path.exists(file_path): + with open(file_path, 'rb') as f: + response = HttpResponse(f.read(), content_type='application/octet-stream') + response['Content-Disposition'] = f'attachment; filename="{quote(value["filename"])}"' + return response + else: + response = { + 'msg': 'File not found' + } + return Response(response, status=404) else: response = { - 'key': kwargs['key'], - 'file': None, - 'filename': None, 'msg': 'Not found' } return Response(response, status=404) - # elif request.method == 'PUT': - # if kwargs['key']: - # request_data = json.loads(request.body) - # new_value = request_data['value'] - # value = redis_instance.get(kwargs['key']) - # if value: - # redis_instance.set(kwargs['key'], new_value) - # response = { - # 'key': kwargs['key'], - # 'file': value, - # 'msg': f"Successfully updated {kwargs['key']}" - # } - # return Response(response, status=200) - # else: - # response = { - # 'key': kwargs['key'], - # 'value': None, - # 'msg': 'Not found' - # } - # return Response(response, status=404) - elif request.method == 'DELETE': - if kwargs['key']: - result = cache.delete(kwargs['key']) - if result == 1: - response = { - 'msg': f"{kwargs['key']} successfully deleted" - } - return Response(response, status=404) + if 'key' in kwargs: + value = cache.get(kwargs['key']) + if value: + if 'path' in value: + file_path = value['path'] + + # Check if the file exists + if os.path.exists(file_path): + # Delete the file + os.remove(file_path) + + # Delete the cache entry + cache.delete(kwargs['key']) + + response = { + 'msg': f"{kwargs['key']} successfully deleted" + } + return Response(response, status=200) + else: + response = { + 'msg': 'File not found' + } + return Response(response, status=404) else: response = { 'key': kwargs['key'], - 'file': None, 'msg': 'Not found' } return Response(response, status=404) +# elif request.method == 'PUT': +# if kwargs['key']: +# request_data = json.loads(request.body) +# new_value = request_data['value'] +# value = redis_instance.get(kwargs['key']) +# if value: +# redis_instance.set(kwargs['key'], new_value) +# response = { +# 'key': kwargs['key'], +# 'file': value, +# 'msg': f"Successfully updated {kwargs['key']}" +# } +# return Response(response, status=200) +# else: +# response = { +# 'key': kwargs['key'], +# 'value': None, +# 'msg': 'Not found' +# } +# return Response(response, status=404) # class FileView(viewsets.ModelViewSet): # queryset = File.objects.all() # serializer_class = FileSerializer