-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support small file uploads and cancellation of upload sessions;…
… modify previous irrational route design
- Loading branch information
Showing
7 changed files
with
307 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package file | ||
|
||
import ( | ||
"io" | ||
"mime/multipart" | ||
"net/http" | ||
|
||
"github.com/gin-gonic/gin" | ||
|
||
"github.com/vvbbnn00/goflet/config" | ||
"github.com/vvbbnn00/goflet/storage" | ||
"github.com/vvbbnn00/goflet/storage/upload" | ||
"github.com/vvbbnn00/goflet/util/log" | ||
) | ||
|
||
// routePostFile handler for POST /file/*path | ||
// @Summary Upload Small File | ||
// @Description Upload a small file using a POST request, {path} should be the relative path of the file, starting from the root directory, e.g. /file/path/to/file.txt | ||
// @Tags File, Upload | ||
// @Param path path string true "File path" | ||
// @Accept multipart/form-data | ||
// @Param file formData file true "File" | ||
// @Success 201 {object} string "Created" | ||
// @Failure 400 {object} string "Bad request" | ||
// @Failure 404 {object} string "File not found or upload not started" | ||
// @Failure 409 {object} string "File completion in progress" | ||
// @Failure 413 {object} string "File too large, please use PUT method to upload large files" | ||
// @Failure 500 {object} string "Internal server error" | ||
// @Router /file/{path} [post] | ||
// @Security Authorization | ||
func routePostFile(c *gin.Context) { | ||
// Set the request body limit | ||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, config.GofletCfg.FileConfig.MaxPostSize) | ||
file, err := c.FormFile("file") | ||
|
||
// If error is not nil and the error is "http: request body too large", return a 413 status code | ||
if err != nil && err.Error() == "http: request body too large" { | ||
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File too large, please use PUT method to upload large files"}) | ||
return | ||
} | ||
if err != nil { | ||
log.Warnf("Error getting file: %s", err.Error()) | ||
c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"}) | ||
return | ||
} | ||
// If the file is not nil, handle the single file upload | ||
handleSingleFileUpload(file, c) | ||
|
||
// Complete the file upload | ||
relativePath := c.GetString("relativePath") | ||
handleCompleteFileUpload(relativePath, c) | ||
} | ||
|
||
// routeDeleteFile handler for DELETE /file/*path | ||
// @Summary Delete File | ||
// @Description Delete a file by path, {path} should be the relative path of the file, starting from the root directory, e.g. /file/path/to/file.txt | ||
// @Tags File | ||
// @Param path path string true "File path" | ||
// @Success 204 {object} string "Deleted" | ||
// @Failure 400 {object} string "Bad request" | ||
// @Failure 404 {object} string "File not found or upload not started" | ||
// @Failure 500 {object} string "Internal server error" | ||
// @Router /file/{path} [delete] | ||
// @Security Authorization | ||
func routeDeleteFile(c *gin.Context) { | ||
fsPath := c.GetString("fsPath") | ||
|
||
err := storage.DeleteFile(fsPath) | ||
if err != nil { | ||
errStr := err.Error() | ||
if errStr == "file_not_found" { | ||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) | ||
return | ||
} | ||
log.Warnf("Error deleting file: %s", errStr) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error deleting file"}) | ||
return | ||
} | ||
|
||
c.Status(http.StatusNoContent) | ||
} | ||
|
||
// handleSingleFileUpload handles the single file upload | ||
func handleSingleFileUpload(file *multipart.FileHeader, c *gin.Context) { | ||
// Get temp file write stream | ||
relativePath := c.GetString("relativePath") | ||
writeStream, err := upload.GetTempFileWriteStream(relativePath) | ||
if err != nil { | ||
errStr := err.Error() | ||
if errStr == "directory_creation" { | ||
c.JSON(http.StatusForbidden, gin.H{"error": "Directory creation not allowed"}) | ||
return | ||
} | ||
log.Warnf("Error getting write stream: %s", errStr) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error writing file"}) | ||
return | ||
} | ||
|
||
// Open the file | ||
fileReader, err := file.Open() | ||
if err != nil { | ||
log.Warnf("Error opening file: %s", err.Error()) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error reading file"}) | ||
return | ||
} | ||
|
||
// Copy the file to the write stream | ||
_, err = io.Copy(writeStream, fileReader) | ||
if err != nil { | ||
log.Warnf("Error copying file: %s", err.Error()) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error writing file"}) | ||
return | ||
} | ||
|
||
// Close the file | ||
_ = fileReader.Close() | ||
// Close the write stream | ||
_ = writeStream.Close() | ||
|
||
// Complete the file upload | ||
handleCompleteFileUpload(relativePath, c) | ||
} | ||
|
||
// handleCompleteFileUpload handles the completion of the file upload | ||
func handleCompleteFileUpload(relativePath string, c *gin.Context) { | ||
err := upload.CompleteFileUpload(relativePath) | ||
if err != nil { | ||
errStr := err.Error() | ||
if errStr == "file_uploading" { | ||
c.JSON(http.StatusConflict, gin.H{"error": "The file completion is in progress"}) | ||
return | ||
} | ||
if errStr == "file_not_found" { | ||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found or upload not started"}) | ||
return | ||
} | ||
log.Warnf("Error completing file upload: %s", errStr) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error completing file upload"}) | ||
return | ||
} | ||
|
||
c.Status(http.StatusCreated) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package file | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"os" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/vvbbnn00/goflet/storage/upload" | ||
"github.com/vvbbnn00/goflet/util" | ||
"github.com/vvbbnn00/goflet/util/log" | ||
) | ||
|
||
// routePutUpload handler for PUT /file/*path | ||
// @Summary Partial File Upload | ||
// @Description Create an upload session with a partial file upload, supports range requests, {path} should be the relative path of the file, starting from the root directory, e.g. /file/path/to/file.txt | ||
// @Tags Upload | ||
// @Accept */* | ||
// @Param path path string true "File path" | ||
// @Success 202 {object} string "Accepted" | ||
// @Failure 400 {object} string "Bad request" | ||
// @Failure 403 {object} string "Directory creation not allowed" | ||
// @Failure 500 {object} string "Internal server error" | ||
// @Router /upload/{path} [put] | ||
// @Security Authorization | ||
func routePutUpload(c *gin.Context) { | ||
relativePath := c.GetString("relativePath") | ||
|
||
// Parse the range | ||
byteStart, byteEnd, _, err := util.HeaderParseRangeUpload(c.GetHeader("Content-Range"), c.GetHeader("Content-Length")) | ||
if err != nil { | ||
c.JSON(http.StatusRequestedRangeNotSatisfiable, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
|
||
// Get the write stream | ||
writeStream, err := upload.GetTempFileWriteStream(relativePath) | ||
if err != nil { | ||
errStr := err.Error() | ||
if errStr == "directory_creation" { | ||
c.JSON(http.StatusForbidden, gin.H{"error": "Directory creation not allowed"}) | ||
return | ||
} | ||
log.Warnf("Error getting write stream: %s", errStr) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error writing file"}) | ||
return | ||
} | ||
defer func() { | ||
if closeErr := writeStream.Close(); closeErr != nil { | ||
log.Warnf("Error closing write stream: %s", closeErr.Error()) | ||
} | ||
}() | ||
|
||
// Write the file | ||
body := c.Request.Body | ||
defer func(body io.ReadCloser) { | ||
_ = body.Close() | ||
}(body) | ||
|
||
// Seek to the start of the range | ||
_, err = writeStream.Seek(byteStart, io.SeekStart) | ||
if err != nil { | ||
log.Warnf("Error seeking write stream: %s", err.Error()) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error writing file"}) | ||
return | ||
} | ||
|
||
// Write the range to the file | ||
written, err := io.CopyN(writeStream, body, byteEnd-byteStart+1) | ||
if err != nil { | ||
log.Warnf("Error writing to file: %s", err.Error()) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error writing file"}) | ||
return | ||
} | ||
if written != byteEnd-byteStart+1 { | ||
log.Warnf("Incomplete write: expected %d bytes, wrote %d bytes", byteEnd-byteStart+1, written) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Incomplete write"}) | ||
return | ||
} | ||
|
||
log.Debugf("Successfully written %d bytes to %s", written, relativePath) | ||
c.Status(http.StatusAccepted) | ||
} | ||
|
||
// routePostUpload handler for POST /upload/*path | ||
// @Summary Complete Partial File Upload | ||
// @Description Complete an upload session with a partial file upload. You should first upload the file with a PUT request, then complete the upload with a POST request, {path} should be the relative path of the file, starting from the root directory, e.g. /file/path/to/file.txt | ||
// @Tags Upload | ||
// @Param path path string true "File path" | ||
// @Success 201 {object} string "Created" | ||
// @Failure 400 {object} string "Bad request" | ||
// @Failure 404 {object} string "File not found or upload not started" | ||
// @Failure 409 {object} string "File completion in progress" | ||
// @Failure 500 {object} string "Internal server error" | ||
// @Router /upload/{path} [post] | ||
// @Security Authorization | ||
func routePostUpload(c *gin.Context) { | ||
// Complete the file upload | ||
relativePath := c.GetString("relativePath") | ||
handleCompleteFileUpload(relativePath, c) | ||
} | ||
|
||
// routeDeleteUpload handler for DELETE /upload/*path | ||
// @Summary Cancel Upload | ||
// @Description Cancel an upload session, {path} should be the relative path of the file, starting from the root directory, e.g. /upload/path/to/file.txt | ||
// @Tags Upload | ||
// @Param path path string true "File path" | ||
// @Success 204 {object} string "Deleted" | ||
// @Failure 400 {object} string "Bad request" | ||
// @Failure 404 {object} string "Upload session not found" | ||
// @Failure 500 {object} string "Internal server error" | ||
// @Router /upload/{path} [delete] | ||
func routeDeleteUpload(c *gin.Context) { | ||
relativePath := c.GetString("relativePath") | ||
err := upload.RemoveTempFile(relativePath) | ||
if err != nil { | ||
if errors.Is(err, os.ErrNotExist) { | ||
c.JSON(http.StatusNotFound, gin.H{"error": "Upload session not found"}) | ||
return | ||
} | ||
errStr := err.Error() | ||
log.Debugf("Error deleting file: %s", errStr) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error deleting file"}) | ||
return | ||
} | ||
|
||
c.Status(http.StatusNoContent) | ||
} |
Oops, something went wrong.