From: Adam Dullage Date: Tue, 6 Feb 2024 13:51:34 +0000 (+0000) Subject: Move filename and URL decisions to the server. Resolves #153. X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=e067679b16012dc8f9eead25bf46e70084fad4f2;p=flatnotes.git Move filename and URL decisions to the server. Resolves #153. --- diff --git a/client/components/NoteViewerEditor.vue b/client/components/NoteViewerEditor.vue index d1a8550..1e1b031 100644 --- a/client/components/NoteViewerEditor.vue +++ b/client/components/NoteViewerEditor.vue @@ -570,34 +570,24 @@ export default { }, uploadImageHook(file, callback) { - // image.png is the default name given to images copied from the clipboard. To avoid conflicts we'll append a timestamp. - if (file.name == "image.png") { - const currentDateString = new Date().toISOString().replace(/:/g, "-"); - file = new File([file], `image-${currentDateString}.png`, { - type: file.type, - }); - } - - // If the user has entered an alt text, use it. Otherwise, use the filename. const altTextInputValue = document.getElementById( "toastuiAltTextInput" )?.value; - const altText = altTextInputValue ? altTextInputValue : file.name; // Upload the image then use the callback to insert the URL into the editor - this.postAttachment(file).then(function (success) { - if (success === true) { - callback(`/attachments/${encodeURIComponent(file.name)}`, altText); + this.postAttachment(file).then(function (data) { + if (data) { + // If the user has entered an alt text, use it. Otherwise, use the filename returned by the API. + const altText = altTextInputValue ? altTextInputValue : data.filename; + callback(data.url, altText); } }); }, postAttachment(file) { - let parent = this; - if (reservedFilenameCharacters.test(file.name)) { this.badFilenameToast("filename"); - return false; + return; } EventBus.$emit("showToast", "success", "Uploading attachment..."); @@ -610,9 +600,9 @@ export default { "Content-Type": "multipart/form-data", }, }) - .then(function () { + .then(function (response) { EventBus.$emit("showToast", "success", "Attachment uploaded ✓"); - return true; + return response.data; }) .catch(function (error) { if (error.response?.status == 409) { @@ -630,7 +620,7 @@ export default { "Failed to upload attachment ✘" ); } - return false; + return; }); }, diff --git a/server/attachments/base.py b/server/attachments/base.py index fd4864f..8d370de 100644 --- a/server/attachments/base.py +++ b/server/attachments/base.py @@ -3,10 +3,12 @@ from abc import ABC, abstractmethod from fastapi import UploadFile from fastapi.responses import FileResponse +from .models import AttachmentCreateResponse + class BaseAttachments(ABC): @abstractmethod - def create(self, file: UploadFile) -> None: + def create(self, file: UploadFile) -> AttachmentCreateResponse: """Create a new attachment.""" pass diff --git a/server/attachments/file_system/file_system.py b/server/attachments/file_system/file_system.py index c57c99c..a1af2ce 100644 --- a/server/attachments/file_system/file_system.py +++ b/server/attachments/file_system/file_system.py @@ -1,5 +1,7 @@ import os import shutil +import urllib.parse +from datetime import datetime from fastapi import UploadFile from fastapi.responses import FileResponse @@ -7,6 +9,7 @@ from fastapi.responses import FileResponse from helpers import get_env, is_valid_filename from ..base import BaseAttachments +from ..models import AttachmentCreateResponse class FileSystemAttachments(BaseAttachments): @@ -19,14 +22,17 @@ class FileSystemAttachments(BaseAttachments): self.storage_path = os.path.join(self.base_path, "attachments") os.makedirs(self.storage_path, exist_ok=True) - def create(self, file: UploadFile) -> None: + def create(self, file: UploadFile) -> AttachmentCreateResponse: """Create a new attachment.""" is_valid_filename(file.filename) - filepath = os.path.join(self.storage_path, file.filename) - if os.path.exists(filepath): - raise FileExistsError(f"'{file.filename}' already exists.") - with open(filepath, "wb") as f: - shutil.copyfileobj(file.file, f) + try: + self._save_file(file) + except FileExistsError: + file.filename = self._datetime_suffix_filename(file.filename) + self._save_file(file) + return AttachmentCreateResponse( + filename=file.filename, url=self._url_for_filename(file.filename) + ) def get(self, filename: str) -> FileResponse: """Get a specific attachment.""" @@ -35,3 +41,18 @@ class FileSystemAttachments(BaseAttachments): if not os.path.isfile(filepath): raise FileNotFoundError(f"'{filename}' not found.") return FileResponse(filepath) + + def _save_file(self, file: UploadFile): + filepath = os.path.join(self.storage_path, file.filename) + with open(filepath, "xb") as f: + shutil.copyfileobj(file.file, f) + + def _datetime_suffix_filename(self, filename: str) -> str: + """Add a timestamp suffix to the filename.""" + timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H-%M-%SZ") + name, ext = os.path.splitext(filename) + return f"{name}_{timestamp}{ext}" + + def _url_for_filename(self, filename: str) -> str: + """Return the URL for the given filename.""" + return f"/attachments/{urllib.parse.quote(filename)}" diff --git a/server/attachments/models.py b/server/attachments/models.py new file mode 100644 index 0000000..2b65ca9 --- /dev/null +++ b/server/attachments/models.py @@ -0,0 +1,6 @@ +from helpers import CustomBaseModel + + +class AttachmentCreateResponse(CustomBaseModel): + filename: str + url: str diff --git a/server/main.py b/server/main.py index 99a8eff..f751a29 100644 --- a/server/main.py +++ b/server/main.py @@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles import api_messages from attachments.base import BaseAttachments +from attachments.models import AttachmentCreateResponse from auth.base import BaseAuth from auth.models import Login, Token from global_config import AuthType, GlobalConfig, GlobalConfigResponseModel @@ -203,7 +204,7 @@ if global_config.auth_type != AuthType.READ_ONLY: @app.post( "/api/attachments", dependencies=auth_deps, - response_model=None, + response_model=AttachmentCreateResponse, ) def post_attachment(file: UploadFile): """Upload an attachment."""