add rename file functionality
authorPhiTux <redacted>
Wed, 10 Sep 2025 13:00:09 +0000 (15:00 +0200)
committerPhiTux <redacted>
Wed, 10 Sep 2025 13:00:09 +0000 (15:00 +0200)
backend/handlers/logs.go
backend/main.go
frontend/src/lib/FileList.svelte
frontend/src/routes/(authed)/+layout.svelte
frontend/src/routes/(authed)/write/+page.svelte

index cacd90f0f7ff55f150c086566b1bc4fb0adf4f52..b6e72db7279e8dbe16e69a16cacd96e7dbe032e2 100644 (file)
@@ -1004,3 +1004,134 @@ func DeleteDay(w http.ResponseWriter, r *http.Request) {
 
        utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
 }
+
+// RenameFileRequest represents the rename file request body
+type RenameFileRequest struct {
+       UUID        string `json:"uuid"`
+       NewFilename string `json:"new_filename"`
+       Day         int    `json:"day"`
+       Month       int    `json:"month"`
+       Year        int    `json:"year"`
+}
+
+// RenameFile handles renaming a file
+func RenameFile(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req RenameFileRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Validate input
+       req.NewFilename = strings.TrimSpace(req.NewFilename)
+       if req.NewFilename == "" {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "New filename cannot be empty",
+               })
+               return
+       }
+
+       if req.UUID == "" {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "File UUID is required",
+               })
+               return
+       }
+
+       // Get month data
+       content, err := utils.GetMonth(userID, req.Year, req.Month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       enc_filename, err := utils.EncryptText(req.NewFilename, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting text: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Find and update the file
+       days, ok := content["days"].([]any)
+       if !ok {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "No days found",
+               })
+               return
+       }
+
+       found := false
+       for _, d := range days {
+               day, ok := d.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok || int(dayNum) != req.Day {
+                       continue
+               }
+
+               files, ok := day["files"].([]any)
+               if !ok {
+                       continue
+               }
+
+               // Find and rename the specific file
+               for _, f := range files {
+                       file, ok := f.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       if uuid, ok := file["uuid_filename"].(string); ok && uuid == req.UUID {
+                               file["enc_filename"] = enc_filename
+                               found = true
+                               break
+                       }
+               }
+
+               if found {
+                       break
+               }
+       }
+
+       if !found {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "File not found",
+               })
+               return
+       }
+
+       // Save the updated month data
+       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       utils.Logger.Printf("File renamed successfully for user %d: %s -> %s", userID, req.UUID, req.NewFilename)
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
+}
index 46ba82281ab7717ee9789d56d2b3c92a993a8042..caabbef1bcb374eccb249cc5b05ba44e3a01b5a8 100644 (file)
@@ -86,6 +86,7 @@ func main() {
        mux.HandleFunc("POST /logs/uploadFile", middleware.RequireAuth(handlers.UploadFile))
        mux.HandleFunc("GET /logs/downloadFile", middleware.RequireAuth(handlers.DownloadFile))
        mux.HandleFunc("GET /logs/deleteFile", middleware.RequireAuth(handlers.DeleteFile))
+       mux.HandleFunc("POST /logs/renameFile", middleware.RequireAuth(handlers.RenameFile))
        mux.HandleFunc("GET /logs/getHistory", middleware.RequireAuth(handlers.GetHistory))
        mux.HandleFunc("GET /logs/bookmarkDay", middleware.RequireAuth(handlers.BookmarkDay))
        mux.HandleFunc("GET /logs/deleteDay", middleware.RequireAuth(handlers.DeleteDay))
index 5503a5294478b1715b07c450c50430bae0f05674..da709c7c0e0d8b59083c1e8f7a612687a5b4c868 100644 (file)
@@ -1,13 +1,17 @@
 <script>
        import { Fa } from 'svelte-fa';
-       import { faTrash } from '@fortawesome/free-solid-svg-icons';
+       import { faTrash, faWrench, faEdit, faSave, faTimes } from '@fortawesome/free-solid-svg-icons';
        import { slide } from 'svelte/transition';
        import { formatBytes } from './helpers.js';
        import { getTranslate } from '@tolgee/svelte';
 
        const { t } = getTranslate();
 
-       let { files, downloadFile, askDeleteFile, deleteAllowed } = $props();
+       let { files, downloadFile, askDeleteFile, deleteAllowed, renameFile } = $props();
+
+       let openOptionsMenu = $state(null); // UUID of file with open options menu
+       let editingFilename = $state(null); // UUID of file being renamed
+       let newFilename = $state('');
 </script>
 
 {#each files as file (file.uuid_filename)}
                </button>
                {#if deleteAllowed}
                        <button
-                               class="p-2 fileBtn deleteFileBtn"
-                               onclick={() => askDeleteFile(file.uuid_filename, file.filename)}
-                               ><Fa icon={faTrash} fw /></button
+                               class="p-2 fileBtn optionsBtn"
+                               onclick={() => {
+                                       if (openOptionsMenu === file.uuid_filename) {
+                                               openOptionsMenu = null;
+                                               editingFilename = null;
+                                       } else {
+                                               openOptionsMenu = file.uuid_filename;
+                                               editingFilename = null;
+                                       }
+                               }}
                        >
+                               <Fa icon={faWrench} fw />
+                       </button>
                {/if}
        </div>
+
+       {#if deleteAllowed && openOptionsMenu === file.uuid_filename}
+               <div transition:slide>
+                       <div class="options-menu p-3 mt-1">
+                               <div class="mb-3">
+                                       <!-- svelte-ignore a11y_label_has_associated_control -->
+                                       <label class="form-label small fw-bold">{$t('fileList.change_filename')}:</label>
+                                       <div class="d-flex gap-2">
+                                               {#if editingFilename === file.uuid_filename}
+                                                       <input
+                                                               type="text"
+                                                               class="form-control form-control-sm"
+                                                               id="newFilename-{file.uuid_filename}"
+                                                               bind:value={newFilename}
+                                                               onkeydown={(e) => {
+                                                                       if (e.key === 'Enter') {
+                                                                               if (renameFile) {
+                                                                                       renameFile(file.uuid_filename, newFilename);
+                                                                               }
+                                                                               editingFilename = null;
+                                                                               openOptionsMenu = null;
+                                                                       } else if (e.key === 'Escape') {
+                                                                               editingFilename = null;
+                                                                       }
+                                                               }}
+                                                       />
+                                                       <button
+                                                               class="btn btn-sm btn-success"
+                                                               onclick={() => {
+                                                                       if (renameFile) {
+                                                                               renameFile(file.uuid_filename, newFilename);
+                                                                       }
+                                                                       editingFilename = null;
+                                                                       openOptionsMenu = null;
+                                                               }}
+                                                       >
+                                                               <Fa icon={faSave} fw />
+                                                       </button>
+                                                       <button
+                                                               class="btn btn-sm btn-secondary"
+                                                               onclick={() => {
+                                                                       editingFilename = null;
+                                                               }}
+                                                       >
+                                                               <Fa icon={faTimes} fw />
+                                                       </button>
+                                               {:else}
+                                                       <input
+                                                               type="text"
+                                                               class="form-control form-control-sm"
+                                                               value={file.filename}
+                                                               disabled
+                                                       />
+                                                       <button
+                                                               class="btn btn-sm btn-primary"
+                                                               onclick={() => {
+                                                                       editingFilename = file.uuid_filename;
+                                                                       newFilename = file.filename;
+                                                               }}
+                                                       >
+                                                               <Fa icon={faEdit} fw />
+                                                       </button>
+                                               {/if}
+                                       </div>
+                               </div>
+
+                               <hr style="color: black;" />
+
+                               <div>
+                                       <button
+                                               class="btn btn-sm btn-danger w-100"
+                                               onclick={() => {
+                                                       askDeleteFile(file.uuid_filename, file.filename);
+                                                       openOptionsMenu = null;
+                                               }}
+                                       >
+                                               <Fa icon={faTrash} fw class="me-2" />
+                                               {$t('fileList.delete_file')}
+                                       </button>
+                               </div>
+                       </div>
+               </div>
+       {/if}
 {/each}
 
 <style>
                background-color: rgba(0, 0, 0, 0.1);
        }
 
-       .deleteFileBtn {
+       .optionsBtn {
                border-left: 1px solid rgba(92, 92, 92, 0.445);
        }
 
-       .deleteFileBtn:hover {
-               color: rgb(165, 0, 0);
+       .optionsBtn:hover {
+               color: rgb(0, 123, 255);
        }
 
        .file {
                border: 0px solid #ececec77;
                border-radius: 5px;
        }
+
+       .options-menu {
+               background-color: rgba(248, 249, 250, 0.95);
+               border: 1px solid rgba(0, 0, 0, 0.125);
+               border-radius: 5px;
+               backdrop-filter: blur(5px);
+       }
 </style>
index f873263041654eda9c7c653e431a8bafbfb5f89c..ad8c698387b9aefa4b6add024c1aa3297a9e3e41 100644 (file)
                                                                                {/if}
                                                                                <div class="d-flex flex-column tagColumn mt-1">
                                                                                        {#each $tags as tag}
-                                                                                               <Tag
-                                                                                                       {tag}
-                                                                                                       isEditable
-                                                                                                       editTag={openTagModal}
-                                                                                                       isDeletable
-                                                                                                       deleteTag={askDeleteTag}
-                                                                                               />
-                                                                                               {#if deleteTagId === tag.id}
-                                                                                                       <div
-                                                                                                               class="alert alert-danger align-items-center"
-                                                                                                               role="alert"
-                                                                                                               transition:slide
-                                                                                                       >
-                                                                                                               <div>
-                                                                                                                       <Fa icon={faTriangleExclamation} fw />
-                                                                                                                       {@html $t('settings.tags.delete_confirmation')}
-                                                                                                               </div>
-                                                                                                               <!-- svelte-ignore a11y_consider_explicit_label -->
-                                                                                                               <div class="d-flex flex-row mt-2">
-                                                                                                                       <button class="btn btn-secondary" onclick={() => (deleteTagId = null)}
-                                                                                                                               >{$t('settings.abort')}
-                                                                                                                       </button>
-                                                                                                                       <button
-                                                                                                                               disabled={isDeletingTag}
-                                                                                                                               class="btn btn-danger ms-3"
-                                                                                                                               onclick={() => deleteTag(tag.id)}
-                                                                                                                               >{$t('settings.delete')}
-                                                                                                                               {#if isDeletingTag}
-                                                                                                                                       <span
-                                                                                                                                               class="spinner-border spinner-border-sm ms-2"
-                                                                                                                                               role="status"
-                                                                                                                                               aria-hidden="true"
-                                                                                                                                       ></span>
-                                                                                                                               {/if}
-                                                                                                                       </button>
+                                                                                               <div class="mt-2">
+                                                                                                       <Tag
+                                                                                                               {tag}
+                                                                                                               isEditable
+                                                                                                               editTag={openTagModal}
+                                                                                                               isDeletable
+                                                                                                               deleteTag={askDeleteTag}
+                                                                                                       />
+                                                                                                       {#if deleteTagId === tag.id}
+                                                                                                               <div transition:slide style="padding-top: 0.5rem">
+                                                                                                                       <div
+                                                                                                                               class="alert alert-danger align-items-center tagAlert"
+                                                                                                                               role="alert"
+                                                                                                                       >
+                                                                                                                               <div>
+                                                                                                                                       <Fa icon={faTriangleExclamation} fw />
+                                                                                                                                       {@html $t('settings.tags.delete_confirmation')}
+                                                                                                                               </div>
+                                                                                                                               <!-- svelte-ignore a11y_consider_explicit_label -->
+                                                                                                                               <div class="d-flex flex-row mt-2">
+                                                                                                                                       <button
+                                                                                                                                               class="btn btn-secondary"
+                                                                                                                                               onclick={() => (deleteTagId = null)}
+                                                                                                                                               >{$t('settings.abort')}
+                                                                                                                                       </button>
+                                                                                                                                       <button
+                                                                                                                                               disabled={isDeletingTag}
+                                                                                                                                               class="btn btn-danger ms-3"
+                                                                                                                                               onclick={() => deleteTag(tag.id)}
+                                                                                                                                               >{$t('settings.delete')}
+                                                                                                                                               {#if isDeletingTag}
+                                                                                                                                                       <span
+                                                                                                                                                               class="spinner-border spinner-border-sm ms-2"
+                                                                                                                                                               role="status"
+                                                                                                                                                               aria-hidden="true"
+                                                                                                                                                       ></span>
+                                                                                                                                               {/if}
+                                                                                                                                       </button>
+                                                                                                                               </div>
+                                                                                                                       </div>
                                                                                                                </div>
-                                                                                                       </div>
-                                                                                               {/if}
+                                                                                                       {/if}
+                                                                                               </div>
                                                                                        {/each}
                                                                                </div>
                                                                        </div>
 </div>
 
 <style>
+       .tagAlert {
+               margin-bottom: 0 !important;
+       }
+
        .modal-header > div > div > button {
                border: none;
                border-radius: 10px !important;
                width: min-content;
        }
 
-       .tagColumn {
-               gap: 0.5rem;
-       }
-
        #selectMode:checked {
                border-color: #da880e;
                background-color: #da880e;
index 1a93b34aed55d7c6b48acf6256da66063fc1f39f..a0670e3db7ea939471538920cee89d265c9467d6 100644 (file)
                                toast.show();
                        });
        }
+
+       function renameFile(uuid_filename, new_filename) {
+               // Validate filename
+               if (!new_filename || new_filename.trim() === '') {
+                       const toast = new bootstrap.Toast(document.getElementById('toastErrorRenamingFile'));
+                       toast.show();
+                       return;
+               }
+
+               new_filename = new_filename.trim();
+
+               axios
+                       .post(API_URL + '/logs/renameFile', {
+                               uuid: uuid_filename,
+                               new_filename: new_filename,
+                               day: $selectedDate.day,
+                               month: $selectedDate.month,
+                               year: $selectedDate.year
+                       })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       // Update local file list
+                                       filesOfDay = filesOfDay.map((file) => {
+                                               if (file.uuid_filename === uuid_filename) {
+                                                       file.filename = new_filename;
+                                               }
+                                               return file;
+                                       });
+
+                                       // Update images list as well
+                                       images = images.map((image) => {
+                                               if (image.uuid_filename === uuid_filename) {
+                                                       image.filename = new_filename;
+                                               }
+                                               return image;
+                                       });
+                               } else {
+                                       const toast = new bootstrap.Toast(document.getElementById('toastErrorRenamingFile'));
+                                       toast.show();
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorRenamingFile'));
+                               toast.show();
+                       });
+       }
 </script>
 
 <DatepickerLogic />
                                >
                                <input type="file" id="fileInput" multiple style="display: none;" onchange={onFileChange} />
 
-                               <FileList files={filesOfDay} {downloadFile} {askDeleteFile} deleteAllowed />
+                               <FileList files={filesOfDay} {downloadFile} {askDeleteFile} {renameFile} deleteAllowed />
                                {#each uploadingFiles as file}
                                        <div>
                                                {file.name}
                                </div>
                        </div>
                </div>
+
+               <div
+                       id="toastErrorRenamingFile"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">{$t('log.toast.error_renaming_file')}</div>
+                       </div>
+               </div>
        </div>
 
        <div class="modal fade" id="modalConfirmDeleteFile" tabindex="-1">
git clone https://git.99rst.org/PROJECT