package server import ( "encoding/json" "fmt" "mime" "os" "path/filepath" "strings" "sync" "warpbox/lib/models" ) var boxManifestMu sync.Mutex func listBoxFiles(boxID string) ([]models.BoxFile, error) { if manifest, err := reconcileBoxManifest(boxID); err == nil && len(manifest.Files) > 0 { files := make([]models.BoxFile, 0, len(manifest.Files)) for _, file := range manifest.Files { files = append(files, decorateBoxFile(boxID, file)) } return files, nil } return listCompletedFilesFromDisk(boxID) } func reconcileBoxManifest(boxID string) (models.BoxManifest, error) { boxManifestMu.Lock() defer boxManifestMu.Unlock() manifest, err := readBoxManifestUnlocked(boxID) if err != nil { return manifest, err } changed := false for index, file := range manifest.Files { path := filepath.Join(boxPath(boxID), file.Name) info, err := os.Stat(path) if err != nil { continue } if file.Status == fileStatusReady && file.Size == info.Size() { continue } // The manifest is the UI source of truth, but disk wins when an upload // was saved and the final status write/response was interrupted. manifest.Files[index].Size = info.Size() manifest.Files[index].MimeType = mimeTypeForFile(path, file.Name) manifest.Files[index].Status = fileStatusReady changed = true } if changed { if err := writeBoxManifestUnlocked(boxID, manifest); err != nil { return manifest, err } } return manifest, nil } func createBoxManifest(boxID string, requests []models.CreateBoxFileRequest) ([]models.BoxFile, error) { usedNames := make(map[string]int, len(requests)) files := make([]models.BoxFile, 0, len(requests)) for _, request := range requests { filename, ok := safeFilename(request.Name) if !ok { return nil, fmt.Errorf("Invalid filename") } filename = uniqueManifestFilename(filename, usedNames) fileID, err := newFileID() if err != nil { return nil, fmt.Errorf("Could not create file id") } mimeType := mime.TypeByExtension(strings.ToLower(filepath.Ext(filename))) if mimeType == "" { mimeType = "application/octet-stream" } files = append(files, models.BoxFile{ ID: fileID, Name: filename, Size: request.Size, MimeType: mimeType, Status: fileStatusWait, }) } manifest := models.BoxManifest{Files: files} if err := writeBoxManifest(boxID, manifest); err != nil { return nil, err } decoratedFiles := make([]models.BoxFile, 0, len(files)) for _, file := range files { decoratedFiles = append(decoratedFiles, decorateBoxFile(boxID, file)) } return decoratedFiles, nil } func markManifestFileStatus(boxID string, fileID string, status string) (models.BoxFile, error) { if status != fileStatusWait && status != fileStatusWork && status != fileStatusReady && status != fileStatusFailed { return models.BoxFile{}, fmt.Errorf("Invalid file status") } boxManifestMu.Lock() defer boxManifestMu.Unlock() manifest, err := readBoxManifestUnlocked(boxID) if err != nil { return models.BoxFile{}, err } for index, file := range manifest.Files { if file.ID != fileID { continue } manifest.Files[index].Status = status if err := writeBoxManifestUnlocked(boxID, manifest); err != nil { return models.BoxFile{}, err } return decorateBoxFile(boxID, manifest.Files[index]), nil } return models.BoxFile{}, fmt.Errorf("File not found") } func readBoxManifest(boxID string) (models.BoxManifest, error) { boxManifestMu.Lock() defer boxManifestMu.Unlock() return readBoxManifestUnlocked(boxID) } func readBoxManifestUnlocked(boxID string) (models.BoxManifest, error) { var manifest models.BoxManifest data, err := os.ReadFile(manifestPath(boxID)) if err != nil { return manifest, err } if err := json.Unmarshal(data, &manifest); err != nil { return manifest, err } return manifest, nil } func writeBoxManifest(boxID string, manifest models.BoxManifest) error { boxManifestMu.Lock() defer boxManifestMu.Unlock() return writeBoxManifestUnlocked(boxID, manifest) } // Manifest writes are serialized because the browser can upload several files // concurrently into the same box. Without this lock, status updates can race. func writeBoxManifestUnlocked(boxID string, manifest models.BoxManifest) error { data, err := json.MarshalIndent(manifest, "", " ") if err != nil { return err } return os.WriteFile(manifestPath(boxID), data, 0644) }