package routes import ( "bytes" "crypto/md5" "database/sql" "errors" "fmt" "foobar/database" "foobar/model" "io" "mime/multipart" "net/http" "os" "path/filepath" "time" "github.com/google/uuid" ) var maxFileSize int64 = 32 * 1000 * 1000 func fileUpload(w http.ResponseWriter, r *http.Request) (redirectURL, error) { boxURL, err := GetBoxURL(r) if err != nil { return noRedirect, err } tx := database.MustBeginTx() defer tx.Rollback() box, err := database.SelectBox(boxURL) if err != nil { if errors.Is(err, sql.ErrNoRows) { // this is a new URL the user wants to claim for themselves editCode := r.FormValue("edit_code") if editCode == "" { return noRedirect, newUserError( "Missing edit code. Box can't be created without it", ) } var ( id = uuid.New() createdAt = time.Now() lastUpdatedAt = createdAt header = readmeTemplate ) newBox := model.NewBox( id, boxURL, editCode, header, false, false, createdAt, lastUpdatedAt, ) err = database.InsertBox(tx, newBox, editCode, header) if err != nil { return noRedirect, err } box = newBox } else { return noRedirect, err } } filesCtx, err := GetFilesFromUploadRequest(r) if err != nil { return noRedirect, err } var files []model.File for _, file := range filesCtx { bytes, err := readFileBytes(file.FileHeader()) if err != nil { return noRedirect, err } mime := model.FileMime(http.DetectContentType(bytes)) if file.Filename() == "README.md" { editCode := r.FormValue("edit_code") if editCode == "" { return noRedirect, newUserError( "Missing edit code. README can't be updated. " + "If you're not attempting to update the header, we'd " + "suggest changing \"README.md\" to a new filename.", ) } if editCode != box.EditCode() { return noRedirect, newUserError( "Wrong edit code.", ) } md := mdToHTML(bytes) err := database.UpdateHeader(tx, box.ID(), md) if err != nil { return noRedirect, err } if err := tx.Commit(); err != nil { return noRedirect, err } // FIXME: we're ignoring all other uploaded files return redirectURL(fmt.Sprintf("/box/%s", box.Url())), nil } else { err = copyBytesToDisk(file.ID(), bytes) if err != nil { return "", err } checksum := calculateMD5(bytes) newFile := model.NewFile( file.ID(), file.Filename(), file.Size(), file.UploadedDate(), mime, checksum, ) files = append(files, newFile) } } for _, file := range files { err = database.InsertFile(tx, box.ID(), file) if err != nil { return noRedirect, err } } if err = tx.Commit(); err != nil { return noRedirect, err } return redirectURL(fmt.Sprintf("/box/%s/inside", box.Url())), nil } func calculateMD5(fileBytes []byte) model.MD5Checksum { return md5.Sum(fileBytes) } func readFileBytes(fileHeader *multipart.FileHeader) ([]byte, error) { fileReader, err := fileHeader.Open() if err != nil { panic(err) } defer fileReader.Close() fileBytes, err := io.ReadAll(fileReader) if err != nil { return nil, err } return fileBytes, nil } func copyBytesToDisk(fileId uuid.UUID, fileBytes []byte) error { fileWriter, err := os.Create(filepath.Join( staticDir, "box", fmt.Sprintf(fileId.String())), ) if err != nil { return err } defer fileWriter.Close() _, err = io.Copy(fileWriter, bytes.NewReader(fileBytes)) if err != nil { return err } return nil } func GetFilesFromUploadRequest(r *http.Request) ([]model.FileUploadContext, error) { err := r.ParseMultipartForm(int64(maxFileSize)) if err != nil { return nil, err } headers, ok := r.MultipartForm.File["file"] if !ok { return nil, newUserError("no file") } var ( files []model.FileUploadContext ) for _, header := range headers { var ( id = uuid.New() uploadedDate = time.Now() size = header.Size filename = model.Filename(header.Filename) mime = model.MimeFromHeader(header.Header) ) fileUpload := model.NewFileUploadContext(id, uploadedDate, size, filename, mime, header) files = append(files, fileUpload) } return files, nil }