diff --git a/.gitignore b/.gitignore index 03dea1d..21d3abe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ .env +docker-compose.staging.yaml + *_templ.txt *_templ.go \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8abb8e2..c93fce8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN npm install -g tailwindcss RUN npm install -g javascript-obfuscator RUN npm install -g clean-css-cli RUN npx tailwindcss -i ./public/input.css -o ./tmp/output.css -RUN javascript-obfuscator ./public/upload.js --compact true --self-defending true --output ./public/upload_obfuscated.js +RUN javascript-obfuscator ./public/main.js --compact true --self-defending true --output ./public/main_obfuscated.js RUN javascript-obfuscator ./public/validatePassword.js --compact true --self-defending true --output ./public/validatePassword_obfuscated.js RUN javascript-obfuscator ./public/websocket.js --compact true --self-defending true --output ./public/websocket_obfuscated.js RUN cleancss -o ./public/output.css ./tmp/output.css @@ -19,7 +19,7 @@ FROM golang:1.22.2-alpine3.19 AS go_builder WORKDIR /src COPY . . COPY --from=node_builder /src/public /src/public -COPY --from=node_builder /src/public/upload_obfuscated.js /src/public/upload.js +COPY --from=node_builder /src/public/main_obfuscated.js /src/public/main.js COPY --from=node_builder /src/public/validatePassword_obfuscated.js /src/public/validatePassword.js COPY --from=node_builder /src/public/websocket_obfuscated.js /src/public/websocket.js @@ -28,7 +28,7 @@ RUN update-ca-certificates RUN go install github.com/a-h/templ/cmd/templ@$(go list -m -f '{{ .Version }}' github.com/a-h/templ) RUN templ generate RUN go build -o ./tmp/main -RUN rm /src/public/validatePassword_obfuscated.js /src/public/upload_obfuscated.js +RUN rm /src/public/validatePassword_obfuscated.js /src/public/main_obfuscated.js /src/public/websocket_obfuscated.js FROM scratch diff --git a/go.mod b/go.mod index b01d232..dad6edb 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 + github.com/redis/go-redis/v9 v9.6.1 github.com/shirou/gopsutil/v3 v3.24.5 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/xlzd/gotp v0.1.0 @@ -31,7 +32,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/redis/go-redis/v9 v9.6.1 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect diff --git a/go.sum b/go.sum index 3adffb8..22155a2 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/handler/upload/initialisation/initialisation.go b/handler/upload/initialisation/initialisation.go deleted file mode 100644 index 15560b4..0000000 --- a/handler/upload/initialisation/initialisation.go +++ /dev/null @@ -1,144 +0,0 @@ -package initialisation - -import ( - "encoding/json" - "errors" - "fmt" - "github.com/fossyy/filekeeper/app" - "io" - "net/http" - "os" - "path/filepath" - - "github.com/fossyy/filekeeper/types" - "github.com/fossyy/filekeeper/types/models" - "github.com/google/uuid" - "gorm.io/gorm" -) - -func POST(w http.ResponseWriter, r *http.Request) { - userSession := r.Context().Value("user").(types.User) - - body, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - var fileInfo types.FileInfo - if err := json.Unmarshal(body, &fileInfo); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - fileData, err := app.Server.Service.GetUserFile(fileInfo.Name, userSession.UserID.String()) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - upload, err := handleNewUpload(userSession, fileInfo) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - fileData = &types.FileWithDetail{ - ID: fileData.ID, - OwnerID: fileData.OwnerID, - Name: fileData.Name, - Size: fileData.Size, - Downloaded: fileData.Downloaded, - } - fileData.Chunk = make(map[string]bool) - fileData.Done = true - saveFolder := filepath.Join("uploads", userSession.UserID.String(), fileData.ID.String(), fileData.Name) - for i := 0; i <= int(fileInfo.Chunk-1); i++ { - fileName := fmt.Sprintf("%s/chunk_%d", saveFolder, i) - - if _, err := os.Stat(fileName); os.IsNotExist(err) { - fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = false - fileData.Done = false - } else { - fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = true - } - } - respondJSON(w, upload) - return - } - respondErrorJSON(w, err, http.StatusInternalServerError) - return - } - - fileData.Chunk = make(map[string]bool) - fileData.Done = true - saveFolder := filepath.Join("uploads", userSession.UserID.String(), fileData.ID.String(), fileData.Name) - for i := 0; i <= int(fileInfo.Chunk-1); i++ { - fileName := fmt.Sprintf("%s/chunk_%d", saveFolder, i) - - if _, err := os.Stat(fileName); os.IsNotExist(err) { - fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = false - fileData.Done = false - } else { - fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = true - } - } - respondJSON(w, fileData) -} - -func handleNewUpload(user types.User, file types.FileInfo) (models.File, error) { - uploadDir := "uploads" - if _, err := os.Stat(uploadDir); os.IsNotExist(err) { - app.Server.Logger.Error(err.Error()) - err := os.Mkdir(uploadDir, os.ModePerm) - if err != nil { - app.Server.Logger.Error(err.Error()) - return models.File{}, err - } - } - - fileID := uuid.New() - ownerID := user.UserID - - currentDir, _ := os.Getwd() - basePath := filepath.Join(currentDir, uploadDir) - saveFolder := filepath.Join(basePath, ownerID.String(), fileID.String()) - if filepath.Dir(saveFolder) != filepath.Join(basePath, ownerID.String()) { - return models.File{}, errors.New("invalid path") - } - - err := os.MkdirAll(saveFolder, os.ModePerm) - if err != nil { - app.Server.Logger.Error(err.Error()) - return models.File{}, err - } - - newFile := models.File{ - ID: fileID, - OwnerID: ownerID, - Name: file.Name, - Size: file.Size, - TotalChunk: file.Chunk - 1, - Downloaded: 0, - } - - err = app.Server.Database.CreateFile(&newFile) - if err != nil { - app.Server.Logger.Error(err.Error()) - return models.File{}, err - } - return newFile, nil -} - -func respondJSON(w http.ResponseWriter, data interface{}) { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(data); err != nil { - handleError(w, err, http.StatusInternalServerError) - } -} - -func respondErrorJSON(w http.ResponseWriter, err error, statusCode int) { - w.WriteHeader(statusCode) - respondJSON(w, map[string]string{"error": err.Error()}) -} - -func handleError(w http.ResponseWriter, err error, statusCode int) { - http.Error(w, err.Error(), statusCode) - app.Server.Logger.Error(err.Error()) -} diff --git a/handler/user/user.go b/handler/user/user.go index 463fcb5..bf967fb 100644 --- a/handler/user/user.go +++ b/handler/user/user.go @@ -1,22 +1,63 @@ package userHandler import ( + "encoding/json" + "errors" + "fmt" "github.com/a-h/templ" "github.com/fossyy/filekeeper/app" - "github.com/fossyy/filekeeper/types" - "github.com/fossyy/filekeeper/view/client/user" - "net/http" - "github.com/fossyy/filekeeper/session" + "github.com/fossyy/filekeeper/types" + "github.com/fossyy/filekeeper/types/models" + "github.com/fossyy/filekeeper/view/client/user" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "gorm.io/gorm" + "net/http" + "os" + "path/filepath" ) var errorMessages = map[string]string{ "password_not_match": "The passwords provided do not match. Please try again.", } +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +type ActionType string + +const ( + UploadNewFile ActionType = "UploadNewFile" + DeleteFile ActionType = "DeleteFile" + Ping ActionType = "Ping" +) + +type WebsocketAction struct { + Action ActionType `json:"action"` +} + +type ActionUploadNewFile struct { + Action string `json:"action"` + Name string `json:"name"` + Size uint64 `json:"size"` + Chunk uint64 `json:"chunk"` + RequestID string `json:"requestID"` +} + func GET(w http.ResponseWriter, r *http.Request) { - var component templ.Component userSession := r.Context().Value("user").(types.User) + if r.Header.Get("upgrade") == "websocket" { + upgrade, err := upgrader.Upgrade(w, r, nil) + if err != nil { + panic(err) + return + } + handlerWS(upgrade, userSession) + } + var component templ.Component sessions, err := session.GetSessions(userSession.Email) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -45,3 +86,159 @@ func GET(w http.ResponseWriter, r *http.Request) { return } } + +func handlerWS(conn *websocket.Conn, userSession types.User) { + defer conn.Close() + var err error + var message []byte + + for { + _, message, err = conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + fmt.Println("Unexpected connection closure:", err) + } else { + fmt.Println("Connection closed:", err) + } + return + } + var action WebsocketAction + err = json.Unmarshal(message, &action) + if err != nil { + fmt.Println("Error unmarshalling WebsocketAction:", err) + sendErrorResponse(conn, action.Action) + continue + } + + switch action.Action { + case UploadNewFile: + var uploadNewFile ActionUploadNewFile + err = json.Unmarshal(message, &uploadNewFile) + if err != nil { + fmt.Println("Error unmarshalling ActionUploadNewFile:", err) + sendErrorResponse(conn, action.Action) + continue + } + var file *models.File + file, err = app.Server.Database.GetUserFile(uploadNewFile.Name, userSession.UserID.String()) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + newFile := models.File{ + ID: uuid.New(), + OwnerID: userSession.UserID, + Name: uploadNewFile.Name, + Size: uploadNewFile.Size, + TotalChunk: uploadNewFile.Chunk, + Downloaded: 0, + } + err := app.Server.Database.CreateFile(&newFile) + if err != nil { + sendErrorResponse(conn, action.Action) + continue + } + fileData := &types.FileWithDetail{ + ID: newFile.ID, + OwnerID: newFile.OwnerID, + Name: newFile.Name, + Size: newFile.Size, + Downloaded: newFile.Downloaded, + } + fileData.Chunk = make(map[string]bool) + fileData.Done = true + saveFolder := filepath.Join("uploads", userSession.UserID.String(), newFile.ID.String(), newFile.Name) + for i := 0; i <= int(newFile.TotalChunk); i++ { + fileName := fmt.Sprintf("%s/chunk_%d", saveFolder, i) + if _, err := os.Stat(fileName); os.IsNotExist(err) { + fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = false + fileData.Done = false + } else { + fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = true + } + } + sendSuccessResponseWithID(conn, action.Action, fileData, uploadNewFile.RequestID) + continue + } else { + sendErrorResponse(conn, action.Action) + continue + } + } + fileData := &types.FileWithDetail{ + ID: file.ID, + OwnerID: file.OwnerID, + Name: file.Name, + Size: file.Size, + Downloaded: file.Downloaded, + Done: false, + } + fileData.Chunk = make(map[string]bool) + fileData.Done = true + saveFolder := filepath.Join("uploads", userSession.UserID.String(), fileData.ID.String(), fileData.Name) + for i := 0; i <= int(file.TotalChunk-1); i++ { + fileName := fmt.Sprintf("%s/chunk_%d", saveFolder, i) + + if _, err := os.Stat(fileName); os.IsNotExist(err) { + fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = false + fileData.Done = false + } else { + fileData.Chunk[fmt.Sprintf("chunk_%d", i)] = true + } + } + sendSuccessResponseWithID(conn, action.Action, fileData, uploadNewFile.RequestID) + continue + case Ping: + sendSuccessResponse(conn, action.Action, map[string]string{"message": "received"}) + continue + } + } +} + +func sendErrorResponse(conn *websocket.Conn, action ActionType) { + response := map[string]interface{}{ + "action": action, + "status": "error", + } + marshal, err := json.Marshal(response) + if err != nil { + fmt.Println("Error marshalling error response:", err) + return + } + err = conn.WriteMessage(websocket.TextMessage, marshal) + if err != nil { + fmt.Println("Error writing error response:", err) + } +} + +func sendSuccessResponse(conn *websocket.Conn, action ActionType, response interface{}) { + responseJSON := map[string]interface{}{ + "action": action, + "status": "success", + "response": response, + } + marshal, err := json.Marshal(responseJSON) + if err != nil { + fmt.Println("Error marshalling success response:", err) + return + } + err = conn.WriteMessage(websocket.TextMessage, marshal) + if err != nil { + fmt.Println("Error writing success response:", err) + } +} + +func sendSuccessResponseWithID(conn *websocket.Conn, action ActionType, response interface{}, responseID string) { + responseJSON := map[string]interface{}{ + "action": action, + "status": "success", + "response": response, + "responseID": responseID, + } + marshal, err := json.Marshal(responseJSON) + if err != nil { + fmt.Println("Error marshalling success response:", err) + return + } + err = conn.WriteMessage(websocket.TextMessage, marshal) + if err != nil { + fmt.Println("Error writing success response:", err) + } +} diff --git a/public/upload.js b/public/main.js similarity index 79% rename from public/upload.js rename to public/main.js index 03e4241..cbd2f63 100644 --- a/public/upload.js +++ b/public/main.js @@ -1,45 +1,60 @@ -document.addEventListener("dragover", function (event) { - event.preventDefault(); -}); +if (!window.mySocket) { + window.mySocket = new WebSocket(`ws://${window.location.host}/user`); -document.addEventListener("drop", async function (event) { - event.preventDefault(); - const file = event.dataTransfer.files[0] - await handleFile(file) -}); + window.mySocket.onopen = function(event) { + console.log('WebSocket is open now.'); + }; -document.getElementById('dropzone-file').addEventListener('change', async function(event) { - event.preventDefault(); - const file = event.target.files[0] - await handleFile(file) -}); + window.mySocket.onmessage = async function(event) { + try { + const data = JSON.parse(event.data); + if (data.action === "UploadNewFile") { + if (data.response.Done === false) { + const file = window.fileIdMap[data.responseID]; + addNewUploadElement(file); + const fileChunks = await splitFile(file, file.chunkSize); + await uploadChunks(file.name, file.size, fileChunks, data.response.Chunk, data.response.ID); + } else { + alert("File already uploaded."); + } + } + } catch (error) { + console.error('Error parsing message data:', error); + } + }; + + window.mySocket.onerror = function(event) { + console.error('WebSocket error observed:', event); + }; + + window.mySocket.onclose = function(event) { + console.log('WebSocket is closed now.'); + }; +} + +function generateUniqueId() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} async function handleFile(file){ const chunkSize = 2 * 1024 * 1024; const chunks = Math.ceil(file.size / chunkSize); + const fileId = generateUniqueId(); const data = JSON.stringify({ + "action": "UploadNewFile", "name": file.name, "size": file.size, "chunk": chunks, + "requestID": fileId, }); + file.chunkSize = chunkSize; + window.fileIdMap = window.fileIdMap || {}; + window.fileIdMap[fileId] = file; - fetch('/upload/init', { - method: 'POST', - body: data, - }).then(async response => { - const responseData = await response.json() - console.log(responseData) - if (responseData.Done === false) { - addNewUploadElement(file) - const fileChunks = await splitFile(file, chunkSize); - await uploadChunks(file.name,file.size, fileChunks, responseData.Chunk, responseData.ID); - } else { - alert("file already uploaded") - } - - }).catch(error => { - console.error('Error uploading file:', error); - }); + window.mySocket.send(data) } function addNewUploadElement(file){ diff --git a/routes/client/routes.go b/routes/client/routes.go index 621911f..3930f00 100644 --- a/routes/client/routes.go +++ b/routes/client/routes.go @@ -15,7 +15,6 @@ import ( signupHandler "github.com/fossyy/filekeeper/handler/signup" signupVerifyHandler "github.com/fossyy/filekeeper/handler/signup/verify" uploadHandler "github.com/fossyy/filekeeper/handler/upload" - "github.com/fossyy/filekeeper/handler/upload/initialisation" userHandler "github.com/fossyy/filekeeper/handler/user" userHandlerResetPassword "github.com/fossyy/filekeeper/handler/user/ResetPassword" userSessionTerminateHandler "github.com/fossyy/filekeeper/handler/user/session/terminate" @@ -118,10 +117,6 @@ func SetupRoutes() *http.ServeMux { middleware.Auth(uploadHandler.POST, w, r) }) - handler.HandleFunc("POST /upload/init", func(w http.ResponseWriter, r *http.Request) { - middleware.Auth(initialisation.POST, w, r) - }) - handler.HandleFunc("GET /download", func(w http.ResponseWriter, r *http.Request) { middleware.Auth(downloadHandler.GET, w, r) }) diff --git a/types/models/models.go b/types/models/models.go index d1dd8aa..c962efa 100644 --- a/types/models/models.go +++ b/types/models/models.go @@ -14,7 +14,7 @@ type File struct { ID uuid.UUID `gorm:"primaryKey;not null;unique"` OwnerID uuid.UUID `gorm:"not null"` Name string `gorm:"not null"` - Size int64 `gorm:"not null"` - TotalChunk int64 `gorm:"not null"` - Downloaded int64 `gorm:"not null;default=0"` + Size uint64 `gorm:"not null"` + TotalChunk uint64 `gorm:"not null"` + Downloaded uint64 `gorm:"not null;default=0"` } diff --git a/types/types.go b/types/types.go index b16ca03..450ac91 100644 --- a/types/types.go +++ b/types/types.go @@ -22,23 +22,23 @@ type User struct { type FileInfo struct { Name string `json:"name"` - Size int64 `json:"size"` - Chunk int64 `json:"chunk"` + Size uint64 `json:"size"` + Chunk uint64 `json:"chunk"` } type FileData struct { ID string Name string Size string - Downloaded int64 + Downloaded uint64 } type FileWithDetail struct { ID uuid.UUID OwnerID uuid.UUID Name string - Size int64 - Downloaded int64 + Size uint64 + Downloaded uint64 Chunk map[string]bool Done bool } diff --git a/utils/utils.go b/utils/utils.go index fd8b1e1..ad60e52 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -87,7 +87,7 @@ func ValidatePassword(password string) bool { return hasNumber && hasUppercase } -func ConvertFileSize(byte int64) string { +func ConvertFileSize(byte uint64) string { if byte < 1024 { return fmt.Sprintf("%d B", byte) } else if byte < 1024*1024 { diff --git a/view/client/download/download.templ b/view/client/download/download.templ index 0357b02..feac8bd 100644 --- a/view/client/download/download.templ +++ b/view/client/download/download.templ @@ -6,7 +6,7 @@ import ( ) templ component(title string, files []types.FileData){ - @layout.Base(title){ + @layout.BaseAuth(title){