package boxstore import ( "fmt" "io" "mime/multipart" "net/url" "os" "path/filepath" "warpbox/lib/helpers" "warpbox/lib/models" ) func ListFiles(boxID string) ([]models.BoxFile, error) { if manifest, err := reconcileManifest(boxID); err == nil && len(manifest.Files) > 0 { return DecorateFiles(boxID, manifest.Files), nil } return listCompletedFilesFromDisk(boxID) } func SaveManifestUpload(boxID string, fileID string, file *multipart.FileHeader) (models.BoxFile, error) { manifestMu.Lock() defer manifestMu.Unlock() manifest, err := readManifestUnlocked(boxID) if err != nil { return models.BoxFile{}, err } if IsExpired(manifest) { return models.BoxFile{}, fmt.Errorf("Box expired") } fileIndex := -1 for index, manifestFile := range manifest.Files { if manifestFile.ID == fileID { fileIndex = index break } } if fileIndex < 0 { return models.BoxFile{}, fmt.Errorf("File not found") } filename := manifest.Files[fileIndex].Name if err := os.MkdirAll(BoxPath(boxID), 0755); err != nil { return models.BoxFile{}, fmt.Errorf("Could not prepare upload box") } destination, ok := SafeBoxFilePath(boxID, filename) if !ok { return models.BoxFile{}, fmt.Errorf("Invalid filename") } if err := saveMultipartFile(file, destination); err != nil { manifest.Files[fileIndex].Status = models.FileStatusFailed startRetentionIfTerminalUnlocked(&manifest) writeManifestUnlocked(boxID, manifest) return models.BoxFile{}, fmt.Errorf("Could not save uploaded file") } manifest.Files[fileIndex].Size = file.Size manifest.Files[fileIndex].MimeType = helpers.MimeTypeForFile(destination, filename) manifest.Files[fileIndex].Status = models.FileStatusReady startRetentionIfTerminalUnlocked(&manifest) if err := writeManifestUnlocked(boxID, manifest); err != nil { return models.BoxFile{}, err } return DecorateFile(boxID, manifest.Files[fileIndex]), nil } func SaveUpload(boxID string, file *multipart.FileHeader) (models.BoxFile, error) { filename, ok := helpers.SafeFilename(file.Filename) if !ok { return models.BoxFile{}, fmt.Errorf("Invalid filename") } boxPath := BoxPath(boxID) if err := os.MkdirAll(boxPath, 0755); err != nil { return models.BoxFile{}, fmt.Errorf("Could not prepare upload box") } filename = helpers.UniqueFilename(boxPath, filename) destination, ok := SafeBoxFilePath(boxID, filename) if !ok { return models.BoxFile{}, fmt.Errorf("Invalid filename") } if err := saveMultipartFile(file, destination); err != nil { return models.BoxFile{}, fmt.Errorf("Could not save uploaded file") } return DecorateFile(boxID, models.BoxFile{ ID: filename, Name: filename, Size: file.Size, MimeType: helpers.MimeTypeForFile(destination, filename), Status: models.FileStatusReady, }), nil } func DecorateFile(boxID string, file models.BoxFile) models.BoxFile { if file.MimeType == "" { if path, ok := SafeBoxFilePath(boxID, file.Name); ok { file.MimeType = helpers.MimeTypeForFile(path, file.Name) } } if file.SizeLabel == "" { file.SizeLabel = helpers.FormatBytes(file.Size) } file.IconPath = IconForMimeType(file.MimeType, file.Name) if file.ThumbnailPath != nil { file.ThumbnailURL = *file.ThumbnailPath } file.DownloadPath = "/box/" + boxID + "/files/" + url.PathEscape(file.Name) file.UploadPath = "/box/" + boxID + "/files/" + url.PathEscape(file.ID) + "/upload" file.IsComplete = file.Status == models.FileStatusReady switch file.Status { case models.FileStatusReady: file.StatusLabel = "Ready" file.Title = "Download " + file.Name case models.FileStatusFailed: file.StatusLabel = "Failed" file.Title = "Failed to upload" case models.FileStatusWork: file.StatusLabel = "Loading" file.Title = "Loading" default: file.Status = models.FileStatusWait file.StatusLabel = "Waiting" file.Title = "Loading" } return file } func DecorateFiles(boxID string, files []models.BoxFile) []models.BoxFile { decorated := make([]models.BoxFile, 0, len(files)) for _, file := range files { decorated = append(decorated, DecorateFile(boxID, file)) } return decorated } func listCompletedFilesFromDisk(boxID string) ([]models.BoxFile, error) { entries, err := os.ReadDir(BoxPath(boxID)) if err != nil { return nil, err } files := make([]models.BoxFile, 0, len(entries)) for _, entry := range entries { if entry.IsDir() || entry.Name() == manifestFile || entry.Type()&os.ModeSymlink != 0 { continue } info, err := entry.Info() if err != nil { return nil, err } if !info.Mode().IsRegular() { continue } name := entry.Name() files = append(files, DecorateFile(boxID, models.BoxFile{ ID: name, Name: name, Size: info.Size(), MimeType: helpers.MimeTypeForFile(filepath.Join(BoxPath(boxID), name), name), Status: models.FileStatusReady, })) } return files, nil } func saveMultipartFile(file *multipart.FileHeader, destination string) error { source, err := file.Open() if err != nil { return err } defer source.Close() target, tempPath, err := createTempSibling(destination) if err != nil { return err } committed := false defer func() { target.Close() if !committed { os.Remove(tempPath) } }() if _, err := io.Copy(target, source); err != nil { return err } if err := target.Close(); err != nil { return err } if err := os.Rename(tempPath, destination); err != nil { return err } committed = true return nil } func createTempSibling(destination string) (*os.File, string, error) { directory := filepath.Dir(destination) if err := os.MkdirAll(directory, 0755); err != nil { return nil, "", err } target, err := os.CreateTemp(directory, ".warpbox-upload-*") if err != nil { return nil, "", err } return target, target.Name(), nil }