file upload/deletion
authorPhiTux <redacted>
Fri, 24 Jan 2025 22:26:36 +0000 (23:26 +0100)
committerPhiTux <redacted>
Fri, 24 Jan 2025 22:26:36 +0000 (23:26 +0100)
backend/server/routers/logs.py
backend/server/utils/fileHandling.py
frontend/src/lib/Datepicker.svelte
frontend/src/routes/write/+page.svelte

index e272015461338500c29813a16cca5441d00d6aca..2c8558f87a546ada251746f72947c85a5f6c4a58 100644 (file)
@@ -91,9 +91,14 @@ async def getLog(date: str, cookie = Depends(users.isLoggedIn)):
     for dayLog in content["days"]:
         if dayLog["day"] == day:
             enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
-            text = security.decrypt_text(dayLog["text"], enc_key)
-            date_written = security.decrypt_text(dayLog["date_written"], enc_key)
-            return {"text": text, "date_written": date_written}
+            if "text" in dayLog.keys():
+                text = security.decrypt_text(dayLog["text"], enc_key)
+                date_written = security.decrypt_text(dayLog["date_written"], enc_key)
+            if "files" in dayLog.keys():
+                for file in dayLog["files"]:
+                    file["filename"] = security.decrypt_text(file["enc_filename"], enc_key)
+                    file["type"] = security.decrypt_text(file["enc_type"], enc_key)
+            return {"text": text, "date_written": date_written, "files": dayLog.get("files", [])}
 
     return {"text": "", "date_written": ""}
 
@@ -242,18 +247,71 @@ async def uploadFile(day: Annotated[int, Form()], month: Annotated[int, Form()],
     
     # save file in log
     content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+
+    enc_filename = security.encrypt_text(file.filename, enc_key)
+    enc_type = security.encrypt_text(file.headers.get("content-type"), enc_key)
+    new_file = {"enc_filename": enc_filename, "uuid_filename": uuid,"size": file.size, "enc_type": enc_type}
+
     if "days" not in content.keys():
         content["days"] = []
-        content["days"].append({"day": day, "files": [uuid]})
-###########
+        content["days"].append({"day": day, "files": [new_file]})
 
+    else:
+        found = False
+        for dayLog in content["days"]:
+            if dayLog["day"] == day:
+                if "files" not in dayLog.keys():
+                    dayLog["files"] = []
+                dayLog["files"].append(new_file)
+                found = True
+                break
+        if not found:
+            content["days"].append({"day": day, "files": [new_file]})
+    
+    if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+        fileHandling.removeFile(cookie["user_id"], uuid)
+        return {"success": False}
 
-    print(file.size)
-    print(file.filename)
-    print(uuid)
-    print(file.headers.get("content-type"))
-    print(day, month, year)
+    return {"success": True}
 
-    # wait 3 s
-    time.sleep(3)
-    return {"success": True}
\ No newline at end of file
+"""
+@router.get("/getFiles")
+async def getFiles(day: int, month: int, year: int, cookie = Depends(users.isLoggedIn)):
+    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+    if "days" not in content.keys():
+        return []
+    
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+    for dayLog in content["days"]:
+        if "day" in dayLog.keys() and dayLog["day"] == day:
+            if "files" in dayLog.keys():
+                for file in dayLog["files"]:
+                    file["filename"] = security.decrypt_text(file["enc_filename"], enc_key)
+                    file["type"] = security.decrypt_text(file["enc_type"], enc_key)
+    
+                return dayLog["files"]
+
+    return []
+"""
+
+@router.get("/deleteFile")
+async def deleteFile(uuid: str, day: int, month: int, year: int, cookie = Depends(users.isLoggedIn)):
+    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+    if "days" not in content.keys():
+        raise HTTPException(status_code=500, detail="Day not found - json error")
+    
+    for dayLog in content["days"]:
+        if dayLog["day"] != day:
+            continue
+        if not "files" in dayLog.keys():
+            break
+        for file in dayLog["files"]:
+            if file["uuid_filename"] == uuid:
+                if not fileHandling.removeFile(cookie["user_id"], uuid):
+                    raise HTTPException(status_code=500, detail="Failed to delete file")
+                dayLog["files"].remove(file)
+                if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+                    raise HTTPException(status_code=500, detail="Failed to write changes of deleted file!")
+                return {"success": True}
+
+    raise HTTPException(status_code=500, detail="Failed to delete file - not found in log")
\ No newline at end of file
index 207880bce6b0684460b9133dd8bd251e47c1e315..0ca7a4c05a69ea9870465c35e7c334c440c08f35 100644 (file)
@@ -73,7 +73,7 @@ def get_months(user_id, year):
         if entry.is_file() and entry.name.endswith(".json"):
             yield entry.name.split(".")[0]
 
-def writeFile(str, user_id, uuid):
+def writeFile(file, user_id, uuid):
     try:
         os.makedirs(os.path.join(settings.data_path, str(user_id), 'files'), exist_ok=True)
         f = open(os.path.join(settings.data_path, str(user_id), 'files', uuid), "w")
@@ -82,7 +82,7 @@ def writeFile(str, user_id, uuid):
         return False
     else:
         with f:
-            f.write(str)
+            f.write(file)
             return True
         
 def removeFile(user_id, uuid):
index 2613ed1853324975aac3d9c7f896e10dfcbc9adf..b2dbaf7c1acbb4cc140177e46aa14517ea134613 100644 (file)
                border-radius: 50%;
                margin: 2px auto;
                user-select: none;
+               --dot-color: rgb(250, 199, 58);
        }
        .day:hover {
                background: #f0f0f0;
                content: '';
                width: 6px;
                height: 6px;
-               background-color: var(--color);
+               background-color: var(--dot-color);
                border-radius: 50%;
                position: absolute;
                bottom: 2px;
 
        .day.mark-dot:not(.selected)::after {
                content: '';
-               width: 6px;
-               height: 6px;
-               background-color: var(--color);
+               width: 7px;
+               height: 7px;
+               background-color: var(--dot-color);
                border-radius: 50%;
                position: absolute;
-               bottom: 2px;
+               bottom: 1px;
        }
 
        .year-input {
index eca3ea8e06a65d81eb438277adc8cff6d48ffb6a..f903fa1c2be1ab343e49a87b227cb47eb939b8a4 100644 (file)
        import '../../../node_modules/tiny-markdown-editor/dist/tiny-mde.css';
        import { API_URL } from '$lib/APIurl.js';
        import DatepickerLogic from '$lib/DatepickerLogic.svelte';
-       import { faCloudArrowUp } from '@fortawesome/free-solid-svg-icons';
+       import { faCloudArrowUp, faTrash } from '@fortawesome/free-solid-svg-icons';
        import Fa from 'svelte-fa';
        import { v4 as uuidv4 } from 'uuid';
+       import { slide } from 'svelte/transition';
 
        axios.interceptors.request.use((config) => {
                config.withCredentials = true;
        let currentLog = $state('');
        let savedLog = $state('');
 
+       let filesOfDay = $state([]);
+
        let logDateWritten = $state('');
 
        let timeout;
                        });
 
                        currentLog = response.data.text;
+                       filesOfDay = response.data.files;
                        savedLog = currentLog;
 
                        tinyMDE.setContent(currentLog);
                                ...config
                        })
                        .then((response) => {
-                               console.log(response);
+                               // append to filesOfDay
+                               filesOfDay = [...filesOfDay, { filename: f.name, size: f.size, uuid_filename: uuid }];
                        })
                        .catch((error) => {
                                console.error(error);
+                               // toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorSavingFile'));
+                               toast.show();
                        })
                        .finally(() => {
                                uploadingFiles = uploadingFiles.filter((file) => file.uuid !== uuid);
                        });
        }
+
+       function formatBytes(bytes) {
+               if (!+bytes) return '0 Bytes';
+
+               const k = 1024;
+               const dm = 2;
+               const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+
+               const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+               return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
+       }
+
+       function downloadFile(uuid) {
+               console.log(uuid);
+       }
+
+       let confirmDelete = $state({ uuid: '', filename: '' });
+       function askDeleteFile(uuid, filename) {
+               confirmDelete = { uuid: uuid, filename: filename };
+
+               const modal = new bootstrap.Modal(document.getElementById('modalConfirmDeleteFile'));
+               modal.show();
+       }
+
+       function deleteFile(uuid) {
+               axios
+                       .get(API_URL + '/logs/deleteFile', {
+                               params: {
+                                       uuid: uuid,
+                                       year: $selectedDate.getFullYear(),
+                                       month: $selectedDate.getMonth() + 1,
+                                       day: $selectedDate.getDate()
+                               }
+                       })
+                       .then((response) => {
+                               filesOfDay = filesOfDay.filter((file) => file.uuid_filename !== uuid);
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                               // toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorDeletingFile'));
+                               toast.show();
+                       });
+       }
 </script>
 
 <DatepickerLogic />
        <div id="right" class="d-flex flex-column">
                <div>Tags</div>
 
-               <div class="files">
-                       <button class="btn btn-secondary" id="uploadBtn" onclick={triggerFileInput}
+               <div class="files d-flex flex-column">
+                       <button
+                               class="btn btn-secondary {filesOfDay.length > 0 ? 'mb-2' : ''}"
+                               id="uploadBtn"
+                               onclick={triggerFileInput}
                                ><Fa icon={faCloudArrowUp} class="me-2" id="uploadIcon" />Upload</button
                        >
                        <input type="file" id="fileInput" multiple style="display: none;" onchange={onFileChange} />
 
+                       {#each filesOfDay as file (file.uuid_filename)}
+                               <div class="btn-group file mt-2" transition:slide>
+                                       <button
+                                               onclick={() => downloadFile(file.uuid_filename)}
+                                               class="p-2 fileBtn d-flex flex-row align-items-center flex-fill"
+                                               ><div class="filename filenameWeight">{file.filename}</div>
+                                               <span class="filesize">({formatBytes(file.size)})</span>
+                                       </button>
+                                       <button
+                                               class="p-2 fileBtn deleteFileBtn"
+                                               onclick={() => askDeleteFile(file.uuid_filename, file.filename)}
+                                               ><Fa icon={faTrash} id="uploadIcon" fw /></button
+                                       >
+                               </div>
+                       {/each}
                        {#each uploadingFiles as file}
                                <div>
                                        {file.name}
                                <div class="toast-body">Fehler beim Suchen!</div>
                        </div>
                </div>
+
+               <div
+                       id="toastErrorSavingFile"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Speichern einer Datei!</div>
+                       </div>
+               </div>
+
+               <div
+                       id="toastErrorDeletingFile"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Löschen einer Datei!</div>
+                       </div>
+               </div>
+       </div>
+
+       <div class="modal fade" id="modalConfirmDeleteFile" tabindex="-1">
+               <div class="modal-dialog modal-dialog-centered">
+                       <div class="modal-content">
+                               <div class="modal-header">
+                                       <h5 class="modal-title">Datei löschen?</h5>
+                                       <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
+                                       ></button>
+                               </div>
+                               <div class="modal-body">
+                                       <p>
+                                               Datei <u><span class="filenameWeight">{confirmDelete.filename}</span></u> wirklich löschen?
+                                       </p>
+                               </div>
+                               <div class="modal-footer">
+                                       <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
+                                       <button
+                                               onclick={() => deleteFile(confirmDelete.uuid)}
+                                               type="button"
+                                               class="btn btn-primary"
+                                               data-bs-dismiss="modal">Löschen</button
+                                       >
+                               </div>
+                       </div>
+               </div>
        </div>
 </div>
 
 <style>
+       :global(.modal.show) {
+               background-color: rgba(80, 80, 80, 0.1) !important;
+               backdrop-filter: blur(2px) saturate(150%);
+       }
+
+       .modal-content {
+               backdrop-filter: blur(8px) saturate(150%);
+               background-color: rgba(219, 219, 219, 0.45);
+       }
+
+       .filenameWeight {
+               font-weight: 550;
+       }
+
+       .filename {
+               padding-right: 0.5rem;
+               word-break: break-word;
+       }
+
+       .filesize {
+               opacity: 0.7;
+               font-size: 0.8rem;
+       }
+
+       .fileBtn {
+               border: 0;
+               background-color: rgba(0, 0, 0, 0);
+               transition: all ease 0.3s;
+       }
+
+       .fileBtn:hover {
+               background-color: rgba(0, 0, 0, 0.1);
+       }
+
+       .deleteFileBtn {
+               border-left: 1px solid rgba(92, 92, 92, 0.445);
+       }
+
+       .deleteFileBtn:hover {
+               color: rgb(165, 0, 0);
+       }
+
+       .file {
+               background-color: rgba(117, 117, 117, 0.45);
+               border: 0px solid #ececec77;
+               border-radius: 5px;
+       }
+
+       .files {
+               margin-right: 2rem;
+               border-radius: 10px;
+               padding: 1rem;
+               backdrop-filter: blur(8px) saturate(150%);
+               background-color: rgba(219, 219, 219, 0.45);
+               border: 1px solid #ececec77;
+       }
+
        :global(#uploadIcon) {
                transition: all ease 0.3s;
        }
 
        :global(#uploadBtn:hover > #uploadIcon) {
-               transform: scale(1.2);
+               transform: scale(1.4);
        }
 
        :global(.TMCommandBar) {
git clone https://git.99rst.org/PROJECT