From 19ff49c917cc4718b018dc759f6c85a34c66ee47 Mon Sep 17 00:00:00 2001 From: moecinnamo Date: Sat, 22 Nov 2025 17:33:40 +0800 Subject: [PATCH] Add file processing --- internal/handler/file.go | 212 ++++++++++++++++++++++++++++++ internal/handler/httperr.go | 47 +++++++ internal/handler/route.go | 1 + internal/handler/static.go | 82 ++++++++++++ internal/handler/web/httperr.html | 0 internal/handler/web/index.html | 0 internal/service/mime.go | 145 ++++++++++++++++++++ internal/service/route.go | 1 - internal/service/struct.go | 14 ++ 9 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 internal/handler/file.go create mode 100644 internal/handler/httperr.go create mode 100644 internal/handler/route.go create mode 100644 internal/handler/static.go create mode 100644 internal/handler/web/httperr.html create mode 100644 internal/handler/web/index.html create mode 100644 internal/service/mime.go delete mode 100644 internal/service/route.go diff --git a/internal/handler/file.go b/internal/handler/file.go new file mode 100644 index 0000000..89bdf27 --- /dev/null +++ b/internal/handler/file.go @@ -0,0 +1,212 @@ +package handler + +import ( + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +// ListFiles function: Traverse all files in the specified directory and return a slice of the file path +func ListFiles(dir string) ([]string, error) { + var files []string + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + files = append(files, path) + } + return nil + }) + return files, err +} + +// Get Content function: Read the content of a specified file and return it +func GetFileContent(filePath string) ([]byte, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + // Get File Size + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + fileSize := fileInfo.Size() + + // Read File Content + fileContent := make([]byte, fileSize) + _, err = file.Read(fileContent) + if err != nil { + return nil, err + } + + return fileContent, nil +} + +// fileHandler function: Handle file requests +func fileHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Server", "MeowMusicEmbeddedServer") + // Obtain the path of the request + filePath := r.URL.Path + + // Check if the request path starts with "/url/" + if strings.HasPrefix(filePath, "/url/") { + // Extract the URL after "/url/" + urlPath := filePath[len("/url/"):] + // Decode the URL path in case it's URL encoded + decodedURL, err := url.QueryUnescape(urlPath) + if err != nil { + NotFoundHandler(w, r) + return + } + // Determine the protocol based on the URL path + var protocol string + if strings.HasPrefix(decodedURL, "http/") { + protocol = "http://" + } else if strings.HasPrefix(decodedURL, "https/") { + protocol = "https://" + } else { + NotFoundHandler(w, r) + return + } + // Remove the protocol part from the decoded URL + decodedURL = strings.TrimPrefix(decodedURL, "http/") + decodedURL = strings.TrimPrefix(decodedURL, "https/") + // Correctly concatenate the protocol with the decoded URL + decodedURL = protocol + decodedURL + // Create a new HTTP request to the decoded URL, without copying headers + req, err := http.NewRequest("GET", decodedURL, nil) + if err != nil { + NotFoundHandler(w, r) + return + } + // Send the request and get the response + client := &http.Client{} + resp, err := client.Do(req) + if err != nil || resp.StatusCode != http.StatusOK { + NotFoundHandler(w, r) + return + } + defer resp.Body.Close() + // Read the response body into a byte slice + fileContent, err := io.ReadAll(resp.Body) + if err != nil { + NotFoundHandler(w, r) + return + } + // Set appropriate Content-Type based on file extension + ext := filepath.Ext(decodedURL) + switch ext { + case ".mp3": + w.Header().Set("Content-Type", "audio/mpeg") + case ".wav": + w.Header().Set("Content-Type", "audio/wav") + case ".flac": + w.Header().Set("Content-Type", "audio/flac") + case ".aac": + w.Header().Set("Content-Type", "audio/aac") + case ".ogg": + w.Header().Set("Content-Type", "audio/ogg") + case ".m4a": + w.Header().Set("Content-Type", "audio/mp4") + case ".amr": + w.Header().Set("Content-Type", "audio/amr") + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") + case ".png": + w.Header().Set("Content-Type", "image/png") + case ".gif": + w.Header().Set("Content-Type", "image/gif") + case ".bmp": + w.Header().Set("Content-Type", "image/bmp") + case ".svg": + w.Header().Set("Content-Type", "image/svg+xml") + case ".webp": + w.Header().Set("Content-Type", "image/webp") + case ".txt": + w.Header().Set("Content-Type", "text/plain") + case ".lrc": + w.Header().Set("Content-Type", "text/plain") + case ".mrc": + w.Header().Set("Content-Type", "text/plain") + case ".json": + w.Header().Set("Content-Type", "application/json") + default: + w.Header().Set("Content-Type", "application/octet-stream") + } + // Write file content to response + w.Write(fileContent) + return + } + + // Construct the complete file path + fullFilePath := filepath.Join("./files", filePath) + + // Try replacing '+' with ' ' and check if the file exists + tempFilePath := strings.ReplaceAll(fullFilePath, "+", " ") + if _, err := os.Stat(tempFilePath); err == nil { + fullFilePath = tempFilePath + } + + // Get file content + fileContent, err := GetFileContent(fullFilePath) + if err != nil { + // If file not found, try replacing ' ' with '+' and check again + tempFilePath = strings.ReplaceAll(fullFilePath, " ", "+") + fileContent, err = GetFileContent(tempFilePath) + if err != nil { + NotFoundHandler(w, r) + return + } + } + + // Set appropriate Content-Type based on file extension + ext := filepath.Ext(filePath) + switch ext { + case ".mp3": + w.Header().Set("Content-Type", "audio/mpeg") + case ".wav": + w.Header().Set("Content-Type", "audio/wav") + case ".flac": + w.Header().Set("Content-Type", "audio/flac") + case ".aac": + w.Header().Set("Content-Type", "audio/aac") + case ".ogg": + w.Header().Set("Content-Type", "audio/ogg") + case ".m4a": + w.Header().Set("Content-Type", "audio/mp4") + case ".amr": + w.Header().Set("Content-Type", "audio/amr") + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") + case ".png": + w.Header().Set("Content-Type", "image/png") + case ".gif": + w.Header().Set("Content-Type", "image/gif") + case ".bmp": + w.Header().Set("Content-Type", "image/bmp") + case ".svg": + w.Header().Set("Content-Type", "image/svg+xml") + case ".webp": + w.Header().Set("Content-Type", "image/webp") + case ".txt": + w.Header().Set("Content-Type", "text/plain") + case ".lrc": + w.Header().Set("Content-Type", "text/plain") + case ".mrc": + w.Header().Set("Content-Type", "text/plain") + case ".json": + w.Header().Set("Content-Type", "application/json") + default: + w.Header().Set("Content-Type", "application/octet-stream") + } + + // Write file content to response + w.Write(fileContent) +} diff --git a/internal/handler/httperr.go b/internal/handler/httperr.go new file mode 100644 index 0000000..e2bbb34 --- /dev/null +++ b/internal/handler/httperr.go @@ -0,0 +1,47 @@ +package handler + +import ( + "log" + "net/http" + + "github.com/OmniX-Space/MeowBox-Core/internal/service" +) + +func NotFoundHandler(w http.ResponseWriter, r *http.Request) {} + +// ErrorHandler Common error response handler +func ErrorHandler(w http.ResponseWriter, r *http.Request, statusCode int) { + loadErrorTemplate() + + w.WriteHeader(statusCode) + SetHeaders(w, "text/html; charset=utf-8") + + var title, message string + switch statusCode { + case http.StatusNotFound: + title = "" + message = "" + case http.StatusInternalServerError: + title = "" + message = "" + case http.StatusBadRequest: + title = "" + message = "" + case http.StatusForbidden: + title = "" + message = "" + default: + title = "Error" + message = "An unexpected error occurred." + } + + data := service.ErrorPageData{ + StatusCode: statusCode, + Title: title, + Message: message, + } + + if err := errorTemplate.Execute(w, data); err != nil { + log.Printf("[Error] Failed to render error page: %v", err) + } +} diff --git a/internal/handler/route.go b/internal/handler/route.go new file mode 100644 index 0000000..abeebd1 --- /dev/null +++ b/internal/handler/route.go @@ -0,0 +1 @@ +package handler diff --git a/internal/handler/static.go b/internal/handler/static.go new file mode 100644 index 0000000..a8488aa --- /dev/null +++ b/internal/handler/static.go @@ -0,0 +1,82 @@ +package handler + +import ( + "embed" + "html/template" + "log" + "net/http" + "strings" + "sync" + + "github.com/OmniX-Space/MeowBox-Core/internal/service" +) + +//go:embed web +var webFiles embed.FS + +//go:embed web/index.html +var pageTemplateContent string + +//go:embed web/httperr.html +var errorTemplateContent string + +var ( + pageTemplate *template.Template + errorTemplate *template.Template + errorOnce sync.Once + pageOnce sync.Once +) + +// loadPageTemplate Initialize template (executed only once) +func loadPageTemplate() { + pageOnce.Do(func() { + var err error + pageTemplate, err = template.New("page").Parse(pageTemplateContent) + if err != nil { + log.Fatalf("[Error] Failed to parse page template: %v", err) + } + }) +} + +// loadErrorTemplate Initialize template (executed only once) +func loadErrorTemplate() { + errorOnce.Do(func() { + var err error + errorTemplate, err = template.New("error").Parse(errorTemplateContent) + if err != nil { + log.Fatalf("[Error] Failed to parse error template: %v", err) + } + }) +} + +// StaticFileHandler Embedded static file service +func StaticFileHandler(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + // Clean path, make sure it starts with "web/" + path = strings.TrimPrefix(path, "/") + if path == "" { + ErrorHandler(w, r, http.StatusNotFound) + return + } + + // Concatenate embed.FS path + filePath := "web/" + path + data, err := webFiles.ReadFile(filePath) + if err != nil { + log.Printf("[Error] Static file not found: %s", filePath) + ErrorHandler(w, r, http.StatusNotFound) + return + } + + // Set Content-Type + contentType := service.GetContentType(path) + SetHeaders(w, contentType) + + _, _ = w.Write(data) +} + +func SetHeaders(w http.ResponseWriter, contentType string) { + w.Header().Set("Content-Type", contentType) + w.Header().Set("Server", "CloudCat-Project") +} diff --git a/internal/handler/web/httperr.html b/internal/handler/web/httperr.html new file mode 100644 index 0000000..e69de29 diff --git a/internal/handler/web/index.html b/internal/handler/web/index.html new file mode 100644 index 0000000..e69de29 diff --git a/internal/service/mime.go b/internal/service/mime.go new file mode 100644 index 0000000..2f643e6 --- /dev/null +++ b/internal/service/mime.go @@ -0,0 +1,145 @@ +package service + +import "strings" + +func GetContentType(filename string) string { + // Convert file names to lowercase for case insensitive comparison + lowerFilename := strings.ToLower(filename) + + switch { + // Text files + case strings.HasSuffix(lowerFilename, ".css"): + return "text/css; charset=utf-8" + case strings.HasSuffix(lowerFilename, ".js"): + return "application/javascript; charset=utf-8" + case strings.HasSuffix(lowerFilename, ".json"): + return "application/json; charset=utf-8" + case strings.HasSuffix(lowerFilename, ".xml"): + return "application/xml; charset=utf-8" + case strings.HasSuffix(lowerFilename, ".html"), strings.HasSuffix(lowerFilename, ".htm"): + return "text/html; charset=utf-8" + case strings.HasSuffix(lowerFilename, ".txt"): + return "text/plain; charset=utf-8" + case strings.HasSuffix(lowerFilename, ".md"): + return "text/markdown; charset=utf-8" + case strings.HasSuffix(lowerFilename, ".csv"): + return "text/csv; charset=utf-8" + + // Image files + case strings.HasSuffix(lowerFilename, ".webp"): + return "image/webp" + case strings.HasSuffix(lowerFilename, ".png"): + return "image/png" + case strings.HasSuffix(lowerFilename, ".jpg"), strings.HasSuffix(lowerFilename, ".jpeg"): + return "image/jpeg" + case strings.HasSuffix(lowerFilename, ".gif"): + return "image/gif" + case strings.HasSuffix(lowerFilename, ".bmp"): + return "image/bmp" + case strings.HasSuffix(lowerFilename, ".ico"): + return "image/x-icon" + case strings.HasSuffix(lowerFilename, ".svg"), strings.HasSuffix(lowerFilename, ".svgz"): + return "image/svg+xml" + case strings.HasSuffix(lowerFilename, ".tiff"), strings.HasSuffix(lowerFilename, ".tif"): + return "image/tiff" + case strings.HasSuffix(lowerFilename, ".avif"): + return "image/avif" + + // Audio files + case strings.HasSuffix(lowerFilename, ".mp3"): + return "audio/mpeg" + case strings.HasSuffix(lowerFilename, ".wav"): + return "audio/wav" + case strings.HasSuffix(lowerFilename, ".ogg"): + return "audio/ogg" + case strings.HasSuffix(lowerFilename, ".flac"): + return "audio/flac" + case strings.HasSuffix(lowerFilename, ".aac"): + return "audio/aac" + case strings.HasSuffix(lowerFilename, ".m4a"): + return "audio/mp4" + + // Video files + case strings.HasSuffix(lowerFilename, ".mp4"): + return "video/mp4" + case strings.HasSuffix(lowerFilename, ".webm"): + return "video/webm" + case strings.HasSuffix(lowerFilename, ".ogg"), strings.HasSuffix(lowerFilename, ".ogv"): + return "video/ogg" + case strings.HasSuffix(lowerFilename, ".mov"): + return "video/quicktime" + case strings.HasSuffix(lowerFilename, ".avi"): + return "video/x-msvideo" + case strings.HasSuffix(lowerFilename, ".wmv"): + return "video/x-ms-wmv" + case strings.HasSuffix(lowerFilename, ".flv"): + return "video/x-flv" + case strings.HasSuffix(lowerFilename, ".mkv"): + return "video/x-matroska" + + // Font files + case strings.HasSuffix(lowerFilename, ".woff"): + return "font/woff" + case strings.HasSuffix(lowerFilename, ".woff2"): + return "font/woff2" + case strings.HasSuffix(lowerFilename, ".ttf"): + return "font/ttf" + case strings.HasSuffix(lowerFilename, ".otf"): + return "font/otf" + + // Archive files + case strings.HasSuffix(lowerFilename, ".zip"): + return "application/zip" + case strings.HasSuffix(lowerFilename, ".rar"): + return "application/x-rar-compressed" + case strings.HasSuffix(lowerFilename, ".gz"): + return "application/gzip" + case strings.HasSuffix(lowerFilename, ".tar"): + return "application/x-tar" + case strings.HasSuffix(lowerFilename, ".7z"): + return "application/x-7z-compressed" + case strings.HasSuffix(lowerFilename, ".bz2"): + return "application/x-bzip2" + case strings.HasSuffix(lowerFilename, ".xz"): + return "application/x-xz" + + // Document files + case strings.HasSuffix(lowerFilename, ".pdf"): + return "application/pdf" + case strings.HasSuffix(lowerFilename, ".doc"): + return "application/msword" + case strings.HasSuffix(lowerFilename, ".docx"): + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + case strings.HasSuffix(lowerFilename, ".xls"): + return "application/vnd.ms-excel" + case strings.HasSuffix(lowerFilename, ".xlsx"): + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + case strings.HasSuffix(lowerFilename, ".ppt"): + return "application/vnd.ms-powerpoint" + case strings.HasSuffix(lowerFilename, ".pptx"): + return "application/vnd.openxmlformats-officedocument.presentationml.presentation" + case strings.HasSuffix(lowerFilename, ".odt"): + return "application/vnd.oasis.opendocument.text" + case strings.HasSuffix(lowerFilename, ".ods"): + return "application/vnd.oasis.opendocument.spreadsheet" + case strings.HasSuffix(lowerFilename, ".odp"): + return "application/vnd.oasis.opendocument.presentation" + + // Other files + case strings.HasSuffix(lowerFilename, ".rtf"): + return "application/rtf" + case strings.HasSuffix(lowerFilename, ".epub"): + return "application/epub+zip" + case strings.HasSuffix(lowerFilename, ".apk"): + return "application/vnd.android.package-archive" + case strings.HasSuffix(lowerFilename, ".exe"): + return "application/x-msdownload" + case strings.HasSuffix(lowerFilename, ".dmg"): + return "application/x-apple-diskimage" + case strings.HasSuffix(lowerFilename, ".iso"): + return "application/x-iso9660-image" + + default: + return "application/octet-stream" + } +} diff --git a/internal/service/route.go b/internal/service/route.go deleted file mode 100644 index 6d43c33..0000000 --- a/internal/service/route.go +++ /dev/null @@ -1 +0,0 @@ -package service diff --git a/internal/service/struct.go b/internal/service/struct.go index f3bd5c0..1fb6289 100644 --- a/internal/service/struct.go +++ b/internal/service/struct.go @@ -27,3 +27,17 @@ type Config struct { Prefix string `json:"prefix"` } `json:"database"` } + +// ErrorPageData Data model for error page template +type ErrorPageData struct { + StatusCode int + Title string + Message string +} + +// IndexPageData Data model for index page template +type IndexPageData struct { + StatusCode int + Title string + I18n string +}