This repository has been archived on 2026-07-15. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
drive/backend/handlers/versioning.go
Elijah 724d70e58b
Some checks failed
Automated Container Build / build-and-push (push) Failing after 12s
Inital build
2026-05-22 12:29:43 -07:00

118 lines
3 KiB
Go

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(&currentMax)
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)
}
}
}