package main import ( "archive/zip" "encoding/json" "errors" "fmt" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/rs/cors" "gorm.io/gorm" "io" "io/ioutil" "log" "net/http" "os" "path/filepath" ) type HTTPError struct { Error error Message string Code int } type endpointREST func(http.ResponseWriter, *http.Request) *HTTPError func (fn endpointREST) ServeHTTP(w http.ResponseWriter, r *http.Request) { if e := fn(w, r); e != nil { // e is *HTTPError, not os.Error. if e.Code == 401 { w.Header().Set("WWW-Authenticate", `Basic realm="Please enter the password"`) } http.Error(w, fmt.Sprintf("%s - %s", (*e).Message, (*e).Error.Error()), (*e).Code) } } ///////////////////////////////// //////////// routes ///////////// ///////////////////////////////// func AllShares(w http.ResponseWriter, _ *http.Request) *HTTPError { fmt.Println("AllShares") db, err := GetDatabase() if err != nil { return &HTTPError{err, "Can't get database", 500} } var shares []Share err = db.Where("is_public = ? AND is_temporary = 0", 1).Find(&shares).Error if err != nil { return &HTTPError{err, "Can't fetch data", 500} } return SendJSON(w, shares) } func GetShare(w http.ResponseWriter, r *http.Request) *HTTPError { fmt.Println("Get Share") vars := mux.Vars(r) shareID, err := uuid.Parse(vars["id"]) if err != nil { return &HTTPError{err, "invalid URL param", 400} } db, err := GetDatabase() if err != nil { return &HTTPError{err, "Can't get database", 500} } var share Share err = db.Preload("Attachments").Where("ID = ?", shareID).First(&share).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &HTTPError{err, "Record not found", 404} } if err != nil { return &HTTPError{err, "Can't fetch data", 500} } if share.IsTemporary == true { return &HTTPError{errors.New("share is not finalized"), "Share is not finalized", 403} } // auth if share.Password != "" { sid, pass, _ := r.BasicAuth() if sid != share.ID.String() { return &HTTPError{errors.New("unauthorized"), "wrong username", 401} } if pass != share.Password { return &HTTPError{errors.New("unauthorized"), "wrong password", 401} } } return SendJSON(w, share) } func DownloadFile(w http.ResponseWriter, r *http.Request) *HTTPError { fmt.Println("Download file") vars := mux.Vars(r) shareID, err := uuid.Parse(vars["id"]) if err != nil { return &HTTPError{err, "invalid URL param", 400} } attID, err := uuid.Parse(vars["att"]) if err != nil { return &HTTPError{err, "invalid URL param", 400} } db, err := GetDatabase() if err != nil { return &HTTPError{err, "Can't get database", 500} } var att Attachment err = db.Where("id = ?", attID.String()).First(&att).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &HTTPError{err, "Record not found", 404} } if err != nil { return &HTTPError{err, "Can't fetch data", 500} } var share Share err = db.Where("id = ?", att.ShareID.String()).First(&share).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &HTTPError{err, "Record not found", 404} } if err != nil { return &HTTPError{err, "Can't fetch data", 500} } // auth if share.Password != "" { sid, pass, _ := r.BasicAuth() if sid != share.ID.String() { return &HTTPError{errors.New("unauthorized"), "wrong username", 401} } if pass != share.Password { return &HTTPError{errors.New("unauthorized"), "wrong password", 401} } } w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", att.Filename)) http.ServeFile(w, r, filepath.Join(config.mediaDir, "data", shareID.String(), attID.String())) return nil } func OpenShare(w http.ResponseWriter, r *http.Request) *HTTPError { fmt.Println("OpenShare") db, err := GetDatabase() if err != nil { return &HTTPError{err, "Can't get database", 500} } // parse body reqBody, err := ioutil.ReadAll(r.Body) if err != nil { return &HTTPError{err, "Request does not contain a valid body", 400} } var newShare Share err = json.Unmarshal(reqBody, &newShare) if err != nil { return &HTTPError{err, "Can't parse body", 400} } newShare.Attachments = nil // dont want attachments yet // create temporary db entrie newShare.IsTemporary = true err = db.Create(&newShare).Error if err != nil { return &HTTPError{err, "Can't create data", 500} } return SendJSON(w, newShare) } func CloseShare(w http.ResponseWriter, r *http.Request) *HTTPError { fmt.Println("CloseShare") vars := mux.Vars(r) shareID, err := uuid.Parse(vars["id"]) if err != nil { return &HTTPError{err, "invalid URL param", 400} } db, err := GetDatabase() if err != nil { return &HTTPError{err, "Can't get database", 500} } // get stuff var share Share err = db.Where("id = ?", shareID.String()).First(&share).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &HTTPError{err, "Record not found", 404} } if err != nil { return &HTTPError{err, "Can't fetch data", 500} } // move files to permanent location oldPath := filepath.Join(config.mediaDir, "temp", shareID.String()) newPath := filepath.Join(config.mediaDir, "data", shareID.String()) err = os.Rename(oldPath, newPath) if err != nil { return &HTTPError{err, "Can't move directory", 500} } // set stuff permanent share.IsTemporary = false err = db.Save(&share).Error if err != nil { return &HTTPError{err, "Can't edit data", 500} } // TODO send mail SendMail(share) // TODO background job //{ // job, err := enqueuer.Enqueue("DeleteShare", nil) // ////deleteIn := time.Now().Sub(*share.Expires) // //deleteIn := 1 // //job, err := enqueuer.EnqueueIn("DeleteShare", int64(deleteIn), map[string]interface{}{ // // "ShareID": share.ID, // //}) // if err != nil { // return &HTTPError{err, "Error creating background job", 500} // } // PrettyPrint(job) //} return SendJSON(w, share) } func UploadAttachment(w http.ResponseWriter, r *http.Request) *HTTPError { fmt.Println("UploadTest") vars := mux.Vars(r) shareID, err := uuid.Parse(vars["id"]) if err != nil { return &HTTPError{err, "invalid URL param", 400} } db, err := GetDatabase() if err != nil { return &HTTPError{err, "Can't get database", 500} } // get share var share Share err = db.Where("id = ?", shareID.String()).First(&share).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &HTTPError{err, "Record not found", 404} } if err != nil { return &HTTPError{err, "Can't fetch data", 500} } if share.IsTemporary != true { return &HTTPError{errors.New("share is not finalized"), "Can't upload to finalized Shares.", 403} } // Parse file from body err = r.ParseMultipartForm(config.chunkSize) // Maximum 10 MB in RAM if err != nil { return &HTTPError{err, "Request does not contain a valid body (parsing form)", 400} } file, handler, err := r.FormFile("file") if err != nil { return &HTTPError{err, "Request does not contain a valid body (parsing file)", 400} } defer file.Close() var att Attachment { // add db entry TODO fehlerbehandlung für die ganze transaction db.Begin() sid, err := uuid.Parse(shareID.String()) if err != nil { return &HTTPError{err, "foreign key shareID not valid", 500} } att.ShareID = sid att.Filename = handler.Filename att.Filesize = handler.Size db.Create(&att) // save file fileBytes, err := ioutil.ReadAll(file) if err != nil { return &HTTPError{err, "cant read file", 500} } err = ioutil.WriteFile(filepath.Join(config.mediaDir, "temp", sid.String(), att.ID.String()), fileBytes, os.ModePerm) if err != nil { db.Rollback() return &HTTPError{err, "cant save file", 500} } db.Commit() } return SendJSON(w, att) } func DownloadZip(w http.ResponseWriter, r *http.Request) *HTTPError { fmt.Println("DownloadZip") vars := mux.Vars(r) shareID, err := uuid.Parse(vars["id"]) if err != nil { return &HTTPError{err, "invalid URL param", 400} } share, er := RetrieveShare(shareID, true) if er != nil { return er } zipWriter := zip.NewWriter(w) for _, file := range share.Attachments { filePath := filepath.Join(config.mediaDir, "data", file.ShareID.String(), file.ID.String()) fileToZip, err := os.Open(filePath) if err != nil { return &HTTPError{err, "error opening file", 500} } info, err := fileToZip.Stat() if err != nil { return &HTTPError{err, "error getting file info", 500} } header, err := zip.FileInfoHeader(info) if err != nil { return &HTTPError{err, "error creating file header", 500} } header.Name = file.Filename header.Method = zip.Deflate writer, err := zipWriter.CreateHeader(header) if err != nil { return &HTTPError{err, "error creating header", 500} } if _, err := io.Copy(writer, fileToZip); err != nil { return &HTTPError{err, "error copying file into zip archive", 500} } if err := fileToZip.Close(); err != nil { return &HTTPError{err, "error closing zip archive", 500} } } err = zipWriter.Close() if err != nil { return &HTTPError{err, "error when closing zip", 500} } return nil } ///////////////////////////////// ////////// functions //////////// ///////////////////////////////// func RetrieveShare(shareID uuid.UUID, withAtt bool) (*Share, *HTTPError) { // TODO move somewhere else db, err := GetDatabase() if err != nil { return nil, &HTTPError{err, "Can't get database", 500} } var share Share if withAtt == true { err = db.Preload("Attachments").Where("ID = ?", shareID).First(&share).Error } else { err = db.Where("ID = ?", shareID).First(&share).Error } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, &HTTPError{err, "Record not found", 404} } if err != nil { return nil, &HTTPError{err, "Can't fetch data", 500} } if share.IsTemporary == true { return nil, &HTTPError{errors.New("share is not finalized"), "Share is not finalized", 403} } return &share, nil } func ConfigureRoutes() { router := mux.NewRouter().StrictSlash(true) handler := cors.Default().Handler(router) router.Handle("/shares", endpointREST(AllShares)).Methods("GET") router.Handle("/shares", endpointREST(OpenShare)).Methods("POST") router.Handle("/share/{id}", endpointREST(GetShare)).Methods("GET") router.Handle("/share/{id}", endpointREST(CloseShare)).Methods("POST") router.Handle("/share/{id}/attachments", endpointREST(UploadAttachment)).Methods("POST") router.Handle("/share/{id}/attachment/{att}", endpointREST(DownloadFile)).Methods("GET") router.Handle("/share/{id}/zip", endpointREST(DownloadZip)).Methods("GET") log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.port), handler)) } func SendJSON(w http.ResponseWriter, res interface{}) *HTTPError { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(res) if err != nil { return &HTTPError{err, "Can't encode data", 500} } return nil }