From d46d0ccad438e5a74bcf31fb2d58c7d28cf56a90 Mon Sep 17 00:00:00 2001 From: Nicolas-ggd Date: Sat, 14 Dec 2024 23:16:21 +0400 Subject: [PATCH] [ADD] metadata & request struct for file handling --- cmd/app.go | 10 --- cmd/app/app.go | 49 ++++++++++++ cmd/app/config.go | 15 ++++ cmd/filestream/main.go | 47 +----------- file.go | 164 +++++++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 + 7 files changed, 234 insertions(+), 55 deletions(-) delete mode 100644 cmd/app.go create mode 100644 cmd/app/app.go create mode 100644 cmd/app/config.go create mode 100644 file.go create mode 100644 go.sum diff --git a/cmd/app.go b/cmd/app.go deleted file mode 100644 index 980fe5a..0000000 --- a/cmd/app.go +++ /dev/null @@ -1,10 +0,0 @@ -package cmd - -import "log" - -// Application struct is a wrap of application, which controls everything and have top management role, everything is united arround this struct. -type Application struct { - Debug *bool - ErrorLog *log.Logger - InfoLog *log.Logger -} diff --git a/cmd/app/app.go b/cmd/app/app.go new file mode 100644 index 0000000..a11a549 --- /dev/null +++ b/cmd/app/app.go @@ -0,0 +1,49 @@ +package app + +import ( + "flag" + "log" + "net/http" + "os" + "time" +) + +var ( + APP_PORT = os.Getenv("APP_PORT") +) + +func App() { + // parse addr flag to dynamicaly change address value + addr := flag.String("addr", APP_PORT, "HTTP network address") + + // parse debug mode to enable debug mode in application + debug := flag.Bool("debug", false, "Enable debug mode") + // define logs + infoLog := log.New(os.Stderr, "INFO:\t", log.Ldate|log.Ltime) + errorLog := log.New(os.Stderr, "ERROR:\t", log.Ldate|log.Ltime|log.Llongfile) + + flag.Parse() + + if *debug { + infoLog.Println("Enabled debug mode...") + } + + _ = &Application{ + Debug: debug, + InfoLog: infoLog, + ErrorLog: errorLog, + } + + srv := http.Server{ + Addr: *addr, + ErrorLog: errorLog, + IdleTimeout: time.Minute, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + err := srv.ListenAndServe() + if err != nil { + errorLog.Fatalln(err) + } +} diff --git a/cmd/app/config.go b/cmd/app/config.go new file mode 100644 index 0000000..2df5b8f --- /dev/null +++ b/cmd/app/config.go @@ -0,0 +1,15 @@ +package app + +import ( + "github.com/Nicolas-ggd/filestream" + "log" +) + +// Application struct is a wrap of application, which controls everything and have top management role, everything is united around this struct. +type Application struct { + Debug *bool + ErrorLog *log.Logger + InfoLog *log.Logger + RFileRequest *fstream.RFileRequest + File *fstream.File +} diff --git a/cmd/filestream/main.go b/cmd/filestream/main.go index 73aabbf..418ce79 100644 --- a/cmd/filestream/main.go +++ b/cmd/filestream/main.go @@ -1,50 +1,7 @@ package main -import ( - "flag" - "github.com/Nicolas-ggd/filestream/cmd" - "log" - "net/http" - "os" - "time" -) - -var ( - APP_PORT = os.Getenv("APP_PORT") -) +import "github.com/Nicolas-ggd/filestream/cmd/app" func main() { - // parse addr flag to dynamicaly change address value - addr := flag.String("addr", APP_PORT, "HTTP network address") - - // parse debug mode to enable debug mode in application - debug := flag.Bool("debug", false, "Enable debug mode") - // define logs - infoLog := log.New(os.Stderr, "INFO:\t", log.Ldate|log.Ltime) - errorLog := log.New(os.Stderr, "ERROR:\t", log.Ldate|log.Ltime|log.Llongfile) - - flag.Parse() - - if *debug { - infoLog.Println("Enabled debug mode...") - } - - _ = &cmd.Application{ - Debug: debug, - InfoLog: infoLog, - ErrorLog: errorLog, - } - - srv := http.Server{ - Addr: *addr, - ErrorLog: errorLog, - IdleTimeout: time.Minute, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - - err := srv.ListenAndServe() - if err != nil { - errorLog.Fatalln(err) - } + app.App() } diff --git a/file.go b/file.go new file mode 100644 index 0000000..2957efb --- /dev/null +++ b/file.go @@ -0,0 +1,164 @@ +package fstream + +import ( + "fmt" + "io" + "log" + "math" + "mime/multipart" + "os" + "path/filepath" + "strings" + + "github.com/google/uuid" +) + +// File struct is final face of uploaded file, it includes necessary field to use them after file is uploaded +type File struct { + // Original uploaded file name + FileName string + // FileUniqueName is unique name + FileUniqueName string + // Uploaded file path + FilePath string + // Uploaded file extension + FileExtension string + // Uploaded file size + FileSize string +} + +// RFileRequest struct is used for http request, use this struct to bind uploaded file +type RFileRequest struct { + // File is an interface to access the file part of a multipart message. + File multipart.File + // A FileHeader describes a file part of a multipart request. + UploadFile *multipart.FileHeader + // Maximum range of chunk uploads + MaxRange int + // Uploaded file size + FileSize int + // Upload directory + UploadDirectory string + // FileUniqueName is identifier to generate unique name for files + FileUniqueName bool +} + +// generateUuidUniqueName function generates unique string using UUID +func (r *File) generateUuidUniqueName(request *RFileRequest) { + ext := filepath.Ext(request.UploadFile.Filename) + + id, err := uuid.NewUUID() + if err != nil { + log.Fatalln(err) + } + + r.FileUniqueName = fmt.Sprintf("%s%s", id.String(), ext) +} + +// RemoveUploadedFile function removes uploaded file from uploaded directory, it takes param and returns nothing: +// +// RFileRequest struct +func RemoveUploadedFile(r *RFileRequest) { + filePath := filepath.Join(r.UploadDirectory, r.UploadFile.Filename) + + e := os.Remove(filePath) + if e != nil { + log.Printf("error removing file: %v", e) + } +} + +// prettyByteSize function is used to concrete the file size +func prettyByteSize(b int) string { + bf := float64(b) + + for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { + if math.Abs(bf) < 1024.0 { + return fmt.Sprintf("%3.1f%sB", bf, unit) + } + bf /= 1024.0 + } + + return fmt.Sprintf("%.1fYiB", bf) +} + +// StoreChunk cares slice of chunks and returns final results and error +// +// File - struct is final version about file information +// +// error - functions cares about errors and returns error +// +// function creates new directory for chunks if it doesn't exist, if directory already exists it appends received chunks in current chunks and if entire file is uploaded then File struct is returned +func StoreChunk(r *RFileRequest) (*File, error) { + var rFile *File + + // Create new directory for uploaded chunk + filePath := filepath.Join(r.UploadDirectory + r.UploadFile.Filename) + + // Create new directory if it doesn't exist + if _, err := os.Stat(r.UploadDirectory); os.IsNotExist(err) { + err = os.MkdirAll(r.UploadDirectory, 0777) + if err != nil { + return nil, fmt.Errorf("failed to create new temporary directory: %v", err) + } + } + + // Open the file for appending and creating if it doesn't exist + f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("error opening file: %v", err) + } + defer func() { + if cerr := f.Close(); cerr != nil { + log.Printf("error closing file: %v", cerr) + } + }() + + // Copy the chunk data to the file + if _, err := io.Copy(f, r.File); err != nil { + return nil, fmt.Errorf("failed to copying file: %v", err) + } + + // If the entire file is uploaded, finalize entire process and return file information + if r.MaxRange >= r.FileSize { + fileInfo, err := f.Stat() + if err != nil { + return nil, fmt.Errorf("failed to stating file: %v", err) + } + + // Calculate file size in bytes + size := prettyByteSize(int(fileInfo.Size())) + + // Bind File struct and return + rFile = &File{ + FileName: r.UploadFile.Filename, + FileUniqueName: r.UploadFile.Filename, + FilePath: filePath, + FileExtension: filepath.Ext(r.UploadFile.Filename), + FileSize: size, + } + } + + return rFile, nil +} + +// IsAllowExtension function checks if file extension is allowed to upload, it takes following params +// +// fileExtensions - array of strings, which is looks like: []string{".jpg", ".jpeg"}, note that this is fileExtensions which is allowed to receive +// +// fileName - string, this parameter is file name which is like ".jpeg", ".jpg" +// +// Returns: +// +// bool - function returns false if extension isn't allowed to receive, it returns true if extension is allowed to receive +func IsAllowExtension(fileExtensions []string, fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + + // range over the received extensions to check if file is ok to accept + for _, allowed := range fileExtensions { + if ext == allowed { + return true + } + } + + return false +} diff --git a/go.mod b/go.mod index 116859f..c22a034 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/Nicolas-ggd/filestream go 1.23.0 + +require github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7790d7c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=