From 6bcedf84f6004ee828434008c1f09530ae8d1ad5 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 7 Aug 2025 22:08:48 +0200 Subject: [PATCH] Added index, improved logic --- .env | 3 ++ .env.example | 3 +- .gitignore | 6 +++- endpoints/download.go | 42 ++++++------------------- endpoints/index.go | 19 ++++++++++++ endpoints/upload.go | 51 +++++++++++++++++++++++++----- main.go | 6 ++-- views/index.html | 72 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 159 insertions(+), 43 deletions(-) create mode 100644 .env create mode 100644 endpoints/index.go create mode 100644 views/index.html diff --git a/.env b/.env new file mode 100644 index 0000000..0d0ce7e --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +PORT=8080 +UPLOAD_DIR=./uploads +BASE_URL=http://localhost:8080 \ No newline at end of file diff --git a/.env.example b/.env.example index be9a0fa..4758c73 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ PORT=8080 UPLOAD_DIR=./uploads -MAX_FILE_SIZE=10MB \ No newline at end of file +MAX_FILE_SIZE=10MB +BASE_URL=http://localhost:8080 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1932111..6ff45a7 100644 --- a/.gitignore +++ b/.gitignore @@ -201,4 +201,8 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,go,macos,windows,linux,git \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,go,macos,windows,linux,git + + +# Application +uploads/** \ No newline at end of file diff --git a/endpoints/download.go b/endpoints/download.go index 3f25129..211d472 100644 --- a/endpoints/download.go +++ b/endpoints/download.go @@ -9,14 +9,22 @@ import ( ) func HandleFileDownload(c *gin.Context) { + fileID := c.Param("fileID") filename := c.Param("filename") + + if fileID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "File ID is required"}) + return + } + if filename == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Filename is required"}) return } uploadsDir := "./uploads" - filePath := filepath.Join(uploadsDir, filename) + fileDir := filepath.Join(uploadsDir, fileID) + filePath := filepath.Join(fileDir, filename) if _, err := os.Stat(filePath); os.IsNotExist(err) { c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) @@ -27,36 +35,6 @@ func HandleFileDownload(c *gin.Context) { c.Header("Content-Transfer-Encoding", "binary") c.Header("Content-Disposition", "attachment; filename="+filename) c.Header("Content-Type", "application/octet-stream") - + c.File(filePath) } - -func ListFiles(c *gin.Context) { - uploadsDir := "./uploads" - - files, err := os.ReadDir(uploadsDir) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read uploads directory"}) - return - } - - var fileList []gin.H - for _, file := range files { - if !file.IsDir() { - info, err := file.Info() - if err != nil { - continue - } - fileList = append(fileList, gin.H{ - "filename": file.Name(), - "size": info.Size(), - "modified": info.ModTime(), - }) - } - } - - c.JSON(http.StatusOK, gin.H{ - "files": fileList, - "count": len(fileList), - }) -} \ No newline at end of file diff --git a/endpoints/index.go b/endpoints/index.go new file mode 100644 index 0000000..0a02a93 --- /dev/null +++ b/endpoints/index.go @@ -0,0 +1,19 @@ +package endpoints + +import ( + "net/http" + "os" + + "github.com/gin-gonic/gin" +) + +func HandleIndex(c *gin.Context) { + baseURL := os.Getenv("BASE_URL") + if baseURL == "" { + baseURL = "http://localhost:8080" + } + + c.HTML(http.StatusOK, "index.html", gin.H{ + "BaseURL": baseURL, + }) +} \ No newline at end of file diff --git a/endpoints/upload.go b/endpoints/upload.go index d7414ba..8136248 100644 --- a/endpoints/upload.go +++ b/endpoints/upload.go @@ -1,7 +1,10 @@ package endpoints import ( + "crypto/rand" + "encoding/hex" "io" + "mime/multipart" "net/http" "os" "path/filepath" @@ -9,27 +12,52 @@ import ( "github.com/gin-gonic/gin" ) +func generateID() (string, error) { + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + func HandleFileUpload(c *gin.Context) { file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"}) return } - defer file.Close() + defer func(file multipart.File) { + err := file.Close() + if err != nil { - uploadsDir := "./uploads" - if err := os.MkdirAll(uploadsDir, 0755); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create uploads directory"}) + } + }(file) + + fileID, err := generateID() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate file ID"}) return } - dst := filepath.Join(uploadsDir, header.Filename) + uploadsDir := "./uploads" + fileDir := filepath.Join(uploadsDir, fileID) + if err := os.MkdirAll(fileDir, 0755); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file directory"}) + return + } + + dst := filepath.Join(fileDir, header.Filename) out, err := os.Create(dst) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file"}) return } - defer out.Close() + defer func(out *os.File) { + err := out.Close() + if err != nil { + + } + }(out) _, err = io.Copy(out, file) if err != nil { @@ -37,9 +65,18 @@ func HandleFileUpload(c *gin.Context) { return } + baseURL := os.Getenv("BASE_URL") + if baseURL == "" { + baseURL = "http://localhost:8080" + } + + fileURL := baseURL + "/files/" + fileID + "/" + header.Filename + c.JSON(http.StatusOK, gin.H{ "message": "File uploaded successfully", + "id": fileID, "filename": header.Filename, "size": header.Size, + "url": fileURL, }) -} \ No newline at end of file +} diff --git a/main.go b/main.go index a4b6a84..8f976b5 100644 --- a/main.go +++ b/main.go @@ -10,10 +10,12 @@ import ( func main() { r := gin.Default() + r.LoadHTMLGlob("views/*") + r.GET("/", endpoints.HandleIndex) r.POST("/upload", endpoints.HandleFileUpload) - r.GET("/download/:filename", endpoints.HandleFileDownload) - r.GET("/files", endpoints.ListFiles) + r.GET("/uploads/:fileID/:filename", endpoints.HandleFileDownload) + r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..fe8ea0e --- /dev/null +++ b/views/index.html @@ -0,0 +1,72 @@ + + + + + + FITRA - File transfer API + + + +

🚀 FITRA - File transfer API

+

Version: 1.0.0

+

Simple file upload and download service for developers using HTTP requests in CLI.

+ +

📋 API endpoints

+ +
+

POST/upload

+

Description: Upload a file

+

cURL Example:

+
curl -X POST -F "file=@/path/to/your/file.txt" {{.BaseURL}}/upload
+
+ +
+

GET/uploads/{fileID}/{filename}

+

Description: Download a file using the ID and filename from upload response

+

cURL Example:

+
curl -O {{.BaseURL}}/uploads/{fileID}/{filename}
+
+ +
+

GET/health

+

Description: Check service health

+

cURL Example:

+
curl {{.BaseURL}}/health
+
+ +

🔄 Usage

+
+ Step 1: Upload a file using POST /upload with form-data 'file' parameter +
+
+ Step 2: Use the returned 'id' and 'filename' to construct download URL +
+
+ Step 3: Download the file using GET /uploads/{id}/{filename} +
+ +

💡 Examples

+
# 1. Upload a file
+curl -X POST -F "file=@myfile.txt" {{.BaseURL}}/upload
+
+# Response will include:
+# {
+#   "id": "abc123...",
+#   "filename": "myfile.txt",
+#   "url": "{{.BaseURL}}/uploads/abc123.../myfile.txt"
+# }
+
+# 2. Download the file
+curl -O {{.BaseURL}}/uploads/abc123.../myfile.txt
+ + \ No newline at end of file