diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index bca3da6..6722a9b 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -29,7 +29,11 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { r.Get("/list-content", handler.ListContent) r.Get("/download-file", handler.DownloadFile) + + r.Post("/upload-file", handler.CreateFolder) r.Get("/create-folder", handler.CreateFolder) + r.Get("/delete-file", handler.DeleteFile) + r.Get("/delete-folder", handler.DeleteFolder) return r } @@ -37,13 +41,13 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router { // accepted path looks like e.g. "/folder1/" or "folder1" func (h *StorageHandler) ListContent(c fiber.Ctx) error { // relative path defaults to root directory - absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - entries_in_list, err := h.storageService.ListContent(absPath) + entries_in_list, err := h.storageService.ListContent(abs_path) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). @@ -54,13 +58,13 @@ func (h *StorageHandler) ListContent(c fiber.Ctx) error { } func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { - absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - f, filename, filesize, err := h.storageService.DownloadFilePrep(absPath) + f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -71,14 +75,69 @@ func (h *StorageHandler) DownloadFile(c fiber.Ctx) error { return c.SendStream(f, int(filesize)) } -func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { - absPath, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) +func (h *StorageHandler) UploadFile(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - err = h.storageService.CreateFolder(absPath, c.Query("name")) + f, err := c.FormFile("document") + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrMissingFileFieldDocument)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrMissingFileFieldDocument))) + } + + err = h.storageService.UploadFile(abs_path, c.Query("name"), f) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + err = c.SaveFile(f, abs_path) + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) CreateFolder(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.CreateFolder(abs_path, c.Query("name")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) DeleteFile(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.DeleteFile(abs_path) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *StorageHandler) DeleteFolder(c fiber.Ctx) error { + abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("path")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + err = h.storageService.DeleteFolder(abs_path) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/repos/storageRepo/storageRepo.go b/app/repos/storageRepo/storageRepo.go index aa60c09..ac838ff 100644 --- a/app/repos/storageRepo/storageRepo.go +++ b/app/repos/storageRepo/storageRepo.go @@ -1,16 +1,20 @@ package storageRepo import ( + "mime/multipart" "os" "git.ma-al.com/goc_daniel/b2b/app/model" ) type UIStorageRepo interface { - EntryInfo(absPath string) (os.FileInfo, error) - ListContent(absPath string) (*[]model.EntryInList, error) - OpenFile(absPath string) (*os.File, error) - CreateFolder(absPath string, name string) error + EntryInfo(abs_path string) (os.FileInfo, error) + ListContent(abs_path string) (*[]model.EntryInList, error) + OpenFile(abs_path string) (*os.File, error) + UploadFile(abs_path string, f *multipart.FileHeader) error + CreateFolder(abs_path string) error + DeleteFile(abs_path string) error + DeleteFolder(abs_path string) error } type StorageRepo struct{} @@ -19,12 +23,12 @@ func New() UIStorageRepo { return &StorageRepo{} } -func (r *StorageRepo) EntryInfo(absPath string) (os.FileInfo, error) { - return os.Stat(absPath) +func (r *StorageRepo) EntryInfo(abs_path string) (os.FileInfo, error) { + return os.Stat(abs_path) } -func (r *StorageRepo) ListContent(absPath string) (*[]model.EntryInList, error) { - entries, err := os.ReadDir(absPath) +func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) { + entries, err := os.ReadDir(abs_path) if err != nil { return nil, err } @@ -42,10 +46,22 @@ func (r *StorageRepo) ListContent(absPath string) (*[]model.EntryInList, error) return &entries_in_list, nil } -func (r *StorageRepo) OpenFile(absPath string) (*os.File, error) { - return os.Open(absPath) +func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) { + return os.Open(abs_path) } -func (r *StorageRepo) CreateFolder(absPath string, name string) error { - os.(absPath) +func (r *StorageRepo) UploadFile(abs_path string, f *multipart.FileHeader) error { + return nil +} + +func (r *StorageRepo) CreateFolder(abs_path string) error { + return os.Mkdir(abs_path, 0755) +} + +func (r *StorageRepo) DeleteFile(abs_path string) error { + return os.Remove(abs_path) +} + +func (r *StorageRepo) DeleteFolder(abs_path string) error { + return os.RemoveAll(abs_path) } diff --git a/app/service/storageService/storageService.go b/app/service/storageService/storageService.go index 963f934..f60f14b 100644 --- a/app/service/storageService/storageService.go +++ b/app/service/storageService/storageService.go @@ -1,6 +1,7 @@ package storageService import ( + "mime/multipart" "os" "path/filepath" "strings" @@ -20,56 +21,110 @@ func New() *StorageService { } } -func (s *StorageService) ListContent(absPath string) (*[]model.EntryInList, error) { - info, err := s.storageRepo.EntryInfo(absPath) +func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) { + info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || !info.IsDir() { return nil, responseErrors.ErrFolderDoesNotExist } - entries_in_list, err := s.storageRepo.ListContent(absPath) + entries_in_list, err := s.storageRepo.ListContent(abs_path) return entries_in_list, err } -func (s *StorageService) DownloadFilePrep(absPath string) (*os.File, string, int64, error) { - info, err := s.storageRepo.EntryInfo(absPath) +func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, int64, error) { + info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || info.IsDir() { return nil, "", 0, responseErrors.ErrFileDoesNotExist } - f, err := s.storageRepo.OpenFile(absPath) + f, err := s.storageRepo.OpenFile(abs_path) if err != nil { return nil, "", 0, err } - return f, filepath.Base(absPath), info.Size(), nil + return f, filepath.Base(abs_path), info.Size(), nil } -func (s *StorageService) CreateFolder(absPath string, name string) error { - info, err := s.storageRepo.EntryInfo(absPath) +func (s *StorageService) UploadFile(abs_path string, name string, f *multipart.FileHeader) error { + info, err := s.storageRepo.EntryInfo(abs_path) if err != nil || !info.IsDir() { return responseErrors.ErrFolderDoesNotExist } - if name == "" || name == "." filepath.Base(name) != name { + if name == "" || name == "." || name == ".." || filepath.Base(name) != name { return responseErrors.ErrBadAttribute } - - absPath2, err := s.AbsPath(absPath, name) + abs_file_path, err := s.AbsPath(abs_path, name) if err != nil { return err } + if abs_file_path == abs_path { + return responseErrors.ErrBadAttribute + } - return s.storageRepo.CreateFolder(absPath, name) + info, err = s.storageRepo.EntryInfo(abs_file_path) + if err == nil { + return responseErrors.ErrNameTaken + } else if os.IsNotExist(err) { + return s.storageRepo.UploadFile(abs_file_path, f) + } else { + return err + } +} + +func (s *StorageService) CreateFolder(abs_path string, name string) error { + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil || !info.IsDir() { + return responseErrors.ErrFolderDoesNotExist + } + + if name == "" || name == "." || name == ".." || filepath.Base(name) != name { + return responseErrors.ErrBadAttribute + } + abs_folder_path, err := s.AbsPath(abs_path, name) + if err != nil { + return err + } + if abs_folder_path == abs_path { + return responseErrors.ErrBadAttribute + } + + info, err = s.storageRepo.EntryInfo(abs_folder_path) + if err == nil { + return responseErrors.ErrNameTaken + } else if os.IsNotExist(err) { + return s.storageRepo.CreateFolder(abs_folder_path) + } else { + return err + } +} + +func (s *StorageService) DeleteFile(abs_path string) error { + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil || info.IsDir() { + return responseErrors.ErrFileDoesNotExist + } + + return s.storageRepo.DeleteFile(abs_path) +} + +func (s *StorageService) DeleteFolder(abs_path string) error { + info, err := s.storageRepo.EntryInfo(abs_path) + if err != nil || !info.IsDir() { + return responseErrors.ErrFolderDoesNotExist + } + + return s.storageRepo.DeleteFolder(abs_path) } // AbsPath extracts an absolute path and validates it func (s *StorageService) AbsPath(root string, relativePath string) (string, error) { - cleanName := filepath.Clean(relativePath) - fullPath := filepath.Join(root, cleanName) + clean_name := filepath.Clean(relativePath) + full_path := filepath.Join(root, clean_name) - if fullPath != root && !strings.HasPrefix(fullPath, root+string(os.PathSeparator)) { + if full_path != root && !strings.HasPrefix(full_path, root+string(os.PathSeparator)) { return "", responseErrors.ErrAccessDenied } - return fullPath, nil + return full_path, nil } diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 65db40a..2ea09e5 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -61,9 +61,11 @@ var ( ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") // Typed errors for storage - ErrAccessDenied = errors.New("access denied!") - ErrFolderDoesNotExist = errors.New("folder does not exist") - ErrFileDoesNotExist = errors.New("file does not exist") + ErrAccessDenied = errors.New("access denied!") + ErrFolderDoesNotExist = errors.New("folder does not exist") + ErrFileDoesNotExist = errors.New("file does not exist") + ErrNameTaken = errors.New("name taken") + ErrMissingFileFieldDocument = errors.New("missing file field 'document'") ) // Error represents an error with HTTP status code @@ -173,6 +175,10 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.folder_does_not_exist") case errors.Is(err, ErrFileDoesNotExist): return i18n.T_(c, "error.file_does_not_exist") + case errors.Is(err, ErrNameTaken): + return i18n.T_(c, "error.name_taken") + case errors.Is(err, ErrMissingFileFieldDocument): + return i18n.T_(c, "error.missing_file_field_document") default: return i18n.T_(c, "error.err_internal_server_error") @@ -218,7 +224,9 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrProductOrItsVariationDoesNotExist), errors.Is(err, ErrAccessDenied), errors.Is(err, ErrFolderDoesNotExist), - errors.Is(err, ErrFileDoesNotExist): + errors.Is(err, ErrFileDoesNotExist), + errors.Is(err, ErrNameTaken), + errors.Is(err, ErrMissingFileFieldDocument): return fiber.StatusBadRequest case errors.Is(err, ErrEmailExists): return fiber.StatusConflict diff --git a/bruno/b2b-daniel/create-folder.yml b/bruno/b2b-daniel/create-folder.yml new file mode 100644 index 0000000..fb04ec1 --- /dev/null +++ b/bruno/b2b-daniel/create-folder.yml @@ -0,0 +1,22 @@ +info: + name: create-folder + type: http + seq: 22 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/create-folder?path&name=../k + params: + - name: path + value: "" + type: query + - name: name + value: ../k + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/delete-entry.yml b/bruno/b2b-daniel/delete-entry.yml new file mode 100644 index 0000000..baf63f2 --- /dev/null +++ b/bruno/b2b-daniel/delete-entry.yml @@ -0,0 +1,19 @@ +info: + name: delete-entry + type: http + seq: 23 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/storage/delete-entry?path=folder2 + params: + - name: path + value: folder2 + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/get-description.yml b/bruno/b2b-daniel/get-description.yml new file mode 100644 index 0000000..bd2137d --- /dev/null +++ b/bruno/b2b-daniel/get-description.yml @@ -0,0 +1,22 @@ +info: + name: get-description + type: http + seq: 24 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=2 + params: + - name: productID + value: "51" + type: query + - name: productLangID + value: "2" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/save-product-description.yml b/bruno/b2b-daniel/save-product-description.yml new file mode 100644 index 0000000..201f4f8 --- /dev/null +++ b/bruno/b2b-daniel/save-product-description.yml @@ -0,0 +1,28 @@ +info: + name: save-product-description + type: http + seq: 25 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=2 + params: + - name: productID + value: "1" + type: query + - name: productLangID + value: "2" + type: query + body: + type: json + data: |- + { + "description": "

Rehabilitation rollers are essential, multi-functional tools used across various exercises and treatments. Their application positively influences the alleviation of injuries and significantly enhances a patient's chances of returning to full physical fitness.

\n

These versatile medical rollers are a staple in movement rehabilitation, corrective gymnastics, and both traditional and sports massages, as their design is perfect for precise limb elevation and separation. They provide critical support, making them ideal for comfortably cushioning the patient's knees, feet, arms, and shoulders.

\n

Support for Children's Development: Rehabilitation bolsters are also highly recommended for children. Incorporating them into play activities offers an engaging way to substantially support the development of gross motor skills.

\n

Customize Your Therapy Space: Thanks to our wide range of colours and various sizes, you can easily assemble a comprehensive exercise kit necessary for every physiotherapy clinic, massage parlour, school, or kindergarten.

\n

\n

Certified Medical Device:

\n

\n

This Rehabilitation Roller is a certified medical device compliant with the essential requirements for medical products and the Medical Devices Act. It has been officially registered with the Office for Registration of Medicinal Products, Medical Devices and Biocidal Products (URPL), is equipped with the manufacturer's Declaration of Conformity, and bears the CE mark.

\n

\"\"

\n
\n

\n

Recommended use:

\n\n
\n

\n

Material Specification & Certification

\n

\n

Cover Material: PVC-coated material specifically designated for medical devices, making it extremely easy to clean and disinfect:

\n\n

\"REACH\"\"Certyfikat\"Nie\"Ognioodporny\"\"Odporny\"Odporny\"Przeznaczony\"Odporny\"Olejoodporny\"

\n

Filling: Medium-firm polyurethane foam with enhanced resistance to deformation.

\n\n

\"Certyfikat\"Atest\"Atest

" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/translate-product-description.yml b/bruno/b2b-daniel/translate-product-description.yml new file mode 100644 index 0000000..f08dc01 --- /dev/null +++ b/bruno/b2b-daniel/translate-product-description.yml @@ -0,0 +1,28 @@ +info: + name: translate-product-description + type: http + seq: 19 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=2&productToLangID=3&model=Google + params: + - name: productID + value: "51" + type: query + - name: productFromLangID + value: "2" + type: query + - name: productToLangID + value: "3" + type: query + - name: model + value: Google + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5