package handlers import ( "fmt" "os" "path/filepath" "strconv" "strings" "time" "git.elijahkuntz.com/Elijah/drive/config" "git.elijahkuntz.com/Elijah/drive/database" ) // CreateVersion checks if a file exists and if so, moves it to the .versions directory // and records it in the database before it gets overwritten. // Call this BEFORE writing the new file to disk. func CreateVersion(db *database.DB, cfg *config.Config, fullPath string, relPath string) error { info, err := os.Stat(fullPath) if err != nil { if os.IsNotExist(err) { return nil // No existing file, nothing to version } return err } if info.IsDir() { return nil // Don't version directories } // Check if versioning is enabled/has a max limit val, err := db.GetSetting("max_versions") maxVersions := 5 if err == nil { if v, e := strconv.Atoi(val); e == nil { maxVersions = v } } if maxVersions <= 0 { return nil // Versioning disabled } // Get current file details from DB (for checksum) var checksum string err = db.QueryRow("SELECT checksum FROM files WHERE path = ?", filepath.ToSlash(relPath)).Scan(&checksum) if err != nil { checksum = "" // fallback } // Generate version path timestamp := time.Now().Format("20060102150405") versionFileName := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filepath.Base(relPath), filepath.Ext(relPath)), timestamp, filepath.Ext(relPath)) versionRelPath := filepath.Join(filepath.Dir(relPath), versionFileName) versionFullPath := filepath.Join(cfg.VersionsDir, versionRelPath) if err := os.MkdirAll(filepath.Dir(versionFullPath), 0755); err != nil { return err } // Move current file to versions directory if err := os.Rename(fullPath, versionFullPath); err != nil { return err } // Determine version number var currentMax int err = db.QueryRow("SELECT COALESCE(MAX(version_number), 0) FROM file_versions WHERE file_path = ?", filepath.ToSlash(relPath)).Scan(¤tMax) if err != nil { currentMax = 0 } versionNumber := currentMax + 1 // Insert version record _, err = db.Exec(` INSERT INTO file_versions (file_path, version_number, version_path, size, checksum) VALUES (?, ?, ?, ?, ?) `, filepath.ToSlash(relPath), versionNumber, filepath.ToSlash(versionRelPath), info.Size(), checksum) if err != nil { return err } // Prune old versions pruneVersions(db, cfg, relPath, maxVersions) return nil } func pruneVersions(db *database.DB, cfg *config.Config, relPath string, maxVersions int) { rows, err := db.Query(` SELECT id, version_path FROM file_versions WHERE file_path = ? ORDER BY version_number DESC LIMIT -1 OFFSET ? `, filepath.ToSlash(relPath), maxVersions) if err != nil { return } defer rows.Close() var idsToDelete []int for rows.Next() { var id int var vPath string if err := rows.Scan(&id, &vPath); err == nil { idsToDelete = append(idsToDelete, id) os.Remove(filepath.Join(cfg.VersionsDir, filepath.FromSlash(vPath))) } } if len(idsToDelete) > 0 { for _, id := range idsToDelete { db.Exec("DELETE FROM file_versions WHERE id = ?", id) } } }