Compare commits
2 Commits
1e02e659ab
...
de94315bd9
| Author | SHA1 | Date | |
|---|---|---|---|
| de94315bd9 | |||
| 1c26770db0 |
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Server Configuration
|
||||||
|
SERVER_HOSTNAME=127.0.0.1
|
||||||
|
SERVER_PORT=8080
|
||||||
|
GIN_MODE=debug
|
||||||
|
|
||||||
|
# Security Configuration
|
||||||
|
JWT_SECRET=your-super-secret-jwt-key-here
|
||||||
|
|
||||||
|
# Sync Configuration
|
||||||
|
SYNC_INTERVAL_MINUTES=1
|
||||||
|
|
||||||
|
# Default Admin User Configuration
|
||||||
|
ADMIN_EMAIL=admin@admin.com
|
||||||
|
ADMIN_PASSWORD=sheetless
|
||||||
|
|
||||||
|
# Directory of your sheets
|
||||||
|
SHEETS_DIRECTORY=/data/sheets
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
sheetless-server
|
sheetless-server
|
||||||
sheetless.db
|
sheetless.db
|
||||||
/result
|
/result
|
||||||
|
/.env
|
||||||
|
|||||||
68
src/config/config.go
Normal file
68
src/config/config.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server struct {
|
||||||
|
Hostname string
|
||||||
|
Port string
|
||||||
|
Mode string // gin.Mode: debug, release, test
|
||||||
|
}
|
||||||
|
JWT struct {
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
Sync struct {
|
||||||
|
Interval time.Duration
|
||||||
|
}
|
||||||
|
Admin struct {
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
SheetsDirectory string
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppConfig *Config
|
||||||
|
|
||||||
|
func Load() {
|
||||||
|
cfg := &Config{}
|
||||||
|
|
||||||
|
// Server configuration
|
||||||
|
cfg.Server.Hostname = getEnv("SERVER_HOSTNAME", "127.0.0.1")
|
||||||
|
cfg.Server.Port = getEnv("SERVER_PORT", "8080")
|
||||||
|
cfg.Server.Mode = getEnv("GIN_MODE", "debug")
|
||||||
|
|
||||||
|
// JWT configuration
|
||||||
|
cfg.JWT.Secret = getEnv("JWT_SECRET", "sheetless-default-jwt-secret-please-change")
|
||||||
|
|
||||||
|
// Sync configuration
|
||||||
|
syncMinutes := getEnvInt("SYNC_INTERVAL_MINUTES", 1)
|
||||||
|
cfg.Sync.Interval = time.Duration(syncMinutes) * time.Minute
|
||||||
|
|
||||||
|
// Admin configuration
|
||||||
|
cfg.Admin.Email = getEnv("ADMIN_EMAIL", "admin@admin.com")
|
||||||
|
cfg.Admin.Password = getEnv("ADMIN_PASSWORD", "sheetless")
|
||||||
|
|
||||||
|
cfg.SheetsDirectory = getEnv("SHEETS_DIRECTORY", "./sheets_directory")
|
||||||
|
|
||||||
|
AppConfig = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"sheetless-server/config"
|
||||||
"sheetless-server/models"
|
"sheetless-server/models"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -12,16 +14,59 @@ var DB *gorm.DB
|
|||||||
|
|
||||||
func InitDatabase() {
|
func InitDatabase() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
DB, err = gorm.Open(sqlite.Open("sheetless.db"), &gorm.Config{})
|
DB, err = gorm.Open(sqlite.Open("sheetless.db"), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to connect to database:", err)
|
log.Fatal("Failed to connect to database:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tables, err := DB.Migrator().GetTables()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to list tables of database:", err)
|
||||||
|
}
|
||||||
|
isNewDatabase := len(tables) == 0
|
||||||
|
|
||||||
// Auto migrate the schema
|
// Auto migrate the schema
|
||||||
err = DB.AutoMigrate(&models.User{}, &models.Sheet{}, &models.Composer{})
|
err = DB.AutoMigrate(&models.User{}, &models.Sheet{}, &models.Composer{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to migrate database:", err)
|
log.Fatal("Failed to migrate database:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isNewDatabase {
|
||||||
|
createDefaultAdminUser()
|
||||||
|
}
|
||||||
|
|
||||||
log.Println("Database connected and migrated successfully")
|
log.Println("Database connected and migrated successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createDefaultAdminUser() {
|
||||||
|
// Check if admin user already exists
|
||||||
|
var existingUser models.User
|
||||||
|
err := DB.Where("email = ?", config.AppConfig.Admin.Email).First(&existingUser).Error
|
||||||
|
if err == nil {
|
||||||
|
// Admin user already exists, don't recreate
|
||||||
|
log.Printf("Admin user already exists: %s", config.AppConfig.Admin.Email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the admin password
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(config.AppConfig.Admin.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to hash admin password: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
adminUser := models.User{
|
||||||
|
Username: "admin",
|
||||||
|
Email: config.AppConfig.Admin.Email,
|
||||||
|
Password: string(hashedPassword),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DB.Create(&adminUser).Error; err != nil {
|
||||||
|
log.Printf("Failed to create admin user: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Default admin user created with email: %s", config.AppConfig.Admin.Email)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sheetless-server/config"
|
||||||
"sheetless-server/database"
|
"sheetless-server/database"
|
||||||
"sheetless-server/models"
|
"sheetless-server/models"
|
||||||
"time"
|
"time"
|
||||||
@@ -11,8 +12,6 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var jwtSecret = []byte("your-secret-key") // TODO: In production, use environment variable
|
|
||||||
|
|
||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
Username string `json:"username" binding:"required"`
|
Username string `json:"username" binding:"required"`
|
||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" binding:"required,email"`
|
||||||
@@ -86,7 +85,7 @@ func Login(c *gin.Context) {
|
|||||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||||
})
|
})
|
||||||
|
|
||||||
tokenString, err := token.SignedString(jwtSecret)
|
tokenString, err := token.SignedString([]byte(config.AppConfig.JWT.Secret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sheetless-server/config"
|
||||||
"sheetless-server/database"
|
"sheetless-server/database"
|
||||||
"sheetless-server/models"
|
"sheetless-server/models"
|
||||||
"sheetless-server/utils"
|
"sheetless-server/utils"
|
||||||
@@ -16,15 +17,6 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const uploadDir = "./uploads"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Create uploads directory if it doesn't exist
|
|
||||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
|
||||||
panic("Failed to create uploads directory: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func UploadSheet(c *gin.Context) {
|
func UploadSheet(c *gin.Context) {
|
||||||
// Get form data
|
// Get form data
|
||||||
title := c.PostForm("title")
|
title := c.PostForm("title")
|
||||||
@@ -69,7 +61,7 @@ func UploadSheet(c *gin.Context) {
|
|||||||
|
|
||||||
// Generate unique filename
|
// Generate unique filename
|
||||||
filename := fmt.Sprintf("%d%s", time.Now().Unix(), filepath.Ext(header.Filename))
|
filename := fmt.Sprintf("%d%s", time.Now().Unix(), filepath.Ext(header.Filename))
|
||||||
filePath := filepath.Join(uploadDir, filename)
|
filePath := filepath.Join(config.AppConfig.SheetsDirectory, filename)
|
||||||
|
|
||||||
// Save file
|
// Save file
|
||||||
out, err := os.Create(filePath)
|
out, err := os.Create(filePath)
|
||||||
@@ -106,15 +98,15 @@ func UploadSheet(c *gin.Context) {
|
|||||||
|
|
||||||
// Create database record
|
// Create database record
|
||||||
sheet := models.Sheet{
|
sheet := models.Sheet{
|
||||||
Uuid: *uuid,
|
Uuid: *uuid,
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: description,
|
Description: description,
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
FileSize: fileInfo.Size(),
|
FileSize: fileInfo.Size(),
|
||||||
FileHash: fileHash,
|
FileHash: fileHash,
|
||||||
ComposerId: composer.Uuid,
|
ComposerUuid: composer.Uuid,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.DB.Create(&sheet).Error; err != nil {
|
if err := database.DB.Create(&sheet).Error; err != nil {
|
||||||
|
|||||||
15
src/main.go
15
src/main.go
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"sheetless-server/config"
|
||||||
"sheetless-server/database"
|
"sheetless-server/database"
|
||||||
"sheetless-server/routes"
|
"sheetless-server/routes"
|
||||||
"sheetless-server/sync"
|
"sheetless-server/sync"
|
||||||
@@ -11,11 +13,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
config.Load()
|
||||||
|
gin.SetMode(config.AppConfig.Server.Mode)
|
||||||
|
|
||||||
database.InitDatabase()
|
database.InitDatabase()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(config.AppConfig.SheetsDirectory, 0755); err != nil {
|
||||||
|
panic("Failed to create uploads directory: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// Start sync runner
|
// Start sync runner
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
ticker := time.NewTicker(config.AppConfig.Sync.Interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -31,8 +40,8 @@ func main() {
|
|||||||
|
|
||||||
routes.SetupRoutes(r)
|
routes.SetupRoutes(r)
|
||||||
|
|
||||||
log.Println("Server starting on port 8080...")
|
log.Printf("Server starting on port %s...", config.AppConfig.Server.Port)
|
||||||
if err := r.Run(":8080"); err != nil {
|
if err := r.Run(config.AppConfig.Server.Hostname + ":" + config.AppConfig.Server.Port); err != nil {
|
||||||
log.Fatal("Failed to start server:", err)
|
log.Fatal("Failed to start server:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sheetless-server/config"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var jwtSecret = []byte("your-secret-key") // Should match the one in handlers/auth.go
|
|
||||||
|
|
||||||
func AuthMiddleware() gin.HandlerFunc {
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
authHeader := c.GetHeader("Authorization")
|
authHeader := c.GetHeader("Authorization")
|
||||||
@@ -29,7 +28,7 @@ func AuthMiddleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
// Parse and validate token
|
// Parse and validate token
|
||||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
return jwtSecret, nil
|
return []byte(config.AppConfig.JWT.Secret), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil || !token.Valid {
|
if err != nil || !token.Valid {
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Sheet struct {
|
type Sheet struct {
|
||||||
Uuid uuid.UUID `json:"uuid" gorm:"type:uuid;primaryKey"`
|
Uuid uuid.UUID `json:"uuid" gorm:"type:uuid;primaryKey"`
|
||||||
Title string `json:"title" gorm:"not null"`
|
Title string `json:"title" gorm:"not null"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
FilePath string `json:"file_path" gorm:"not null"`
|
FilePath string `json:"file_path" gorm:"not null"`
|
||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
FileHash uint64 `json:"file_hash"`
|
FileHash string `json:"file_hash"`
|
||||||
ComposerId uuid.UUID `json:"composer_id"`
|
ComposerUuid uuid.UUID `json:"composer_uuid"`
|
||||||
Composer Composer `json:"composer" gorm:"foreignKey:ComposerId"`
|
Composer Composer `json:"composer" gorm:"foreignKey:ComposerUuid"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sheetless-server/config"
|
||||||
"sheetless-server/database"
|
"sheetless-server/database"
|
||||||
"sheetless-server/handlers"
|
"sheetless-server/handlers"
|
||||||
"sheetless-server/models"
|
"sheetless-server/models"
|
||||||
"sheetless-server/utils"
|
"sheetless-server/utils"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const uploadDir = "./uploads"
|
|
||||||
|
|
||||||
func SyncSheets() error {
|
func SyncSheets() error {
|
||||||
|
log.Println("Running library sync")
|
||||||
|
syncStartTime := time.Now()
|
||||||
|
|
||||||
// Get all sheets
|
// Get all sheets
|
||||||
var sheets []models.Sheet
|
var sheets []models.Sheet
|
||||||
if err := database.DB.Find(&sheets).Error; err != nil {
|
if err := database.DB.Find(&sheets).Error; err != nil {
|
||||||
@@ -22,33 +25,31 @@ func SyncSheets() error {
|
|||||||
|
|
||||||
// Maps
|
// Maps
|
||||||
pathsInDb := make(map[string]*models.Sheet)
|
pathsInDb := make(map[string]*models.Sheet)
|
||||||
hashToSheets := make(map[uint64][]*models.Sheet)
|
hashToSheets := make(map[string][]*models.Sheet)
|
||||||
for i := range sheets {
|
for i := range sheets {
|
||||||
sheet := &sheets[i]
|
sheet := &sheets[i]
|
||||||
pathsInDb[sheet.FilePath] = sheet
|
pathsInDb[sheet.FilePath] = sheet
|
||||||
hashToSheets[sheet.FileHash] = append(hashToSheets[sheet.FileHash], sheet)
|
hashToSheets[sheet.FileHash] = append(hashToSheets[sheet.FileHash], sheet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk uploads dir
|
numFilesWithNewHash := 0
|
||||||
files, err := os.ReadDir(uploadDir)
|
numRenamedFiles := 0
|
||||||
if err != nil {
|
numNewFiles := 0
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
// Walk sheets directory recursively for PDF files
|
||||||
if file.IsDir() {
|
err := filepath.Walk(config.AppConfig.SheetsDirectory, func(filePath string, info os.FileInfo, walkErr error) error {
|
||||||
continue
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip directories and non-PDF files
|
||||||
|
if info.IsDir() || filepath.Ext(filePath) != ".pdf" {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
filePath := filepath.Join(uploadDir, file.Name())
|
|
||||||
hash, err := utils.FileHash(filePath)
|
hash, err := utils.FileHash(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error hashing file %s: %v", filePath, err)
|
log.Printf("Error hashing file %s: %v", filePath, err)
|
||||||
continue
|
return nil
|
||||||
}
|
|
||||||
info, err := file.Info()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting file info %s: %v", filePath, err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingSheet, exists := pathsInDb[filePath]
|
existingSheet, exists := pathsInDb[filePath]
|
||||||
@@ -59,6 +60,8 @@ func SyncSheets() error {
|
|||||||
existingSheet.UpdatedAt = time.Now()
|
existingSheet.UpdatedAt = time.Now()
|
||||||
if err := database.DB.Save(existingSheet).Error; err != nil {
|
if err := database.DB.Save(existingSheet).Error; err != nil {
|
||||||
log.Printf("Error updating sheet hash for %s: %v", filePath, err)
|
log.Printf("Error updating sheet hash for %s: %v", filePath, err)
|
||||||
|
} else {
|
||||||
|
numFilesWithNewHash++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -71,6 +74,8 @@ func SyncSheets() error {
|
|||||||
s.UpdatedAt = time.Now()
|
s.UpdatedAt = time.Now()
|
||||||
if err := database.DB.Save(s).Error; err != nil {
|
if err := database.DB.Save(s).Error; err != nil {
|
||||||
log.Printf("Error updating sheet path for %s: %v", filePath, err)
|
log.Printf("Error updating sheet path for %s: %v", filePath, err)
|
||||||
|
} else {
|
||||||
|
numRenamedFiles++
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -80,11 +85,11 @@ func SyncSheets() error {
|
|||||||
uuid, err := handlers.GenerateNonexistentSheetUuid()
|
uuid, err := handlers.GenerateNonexistentSheetUuid()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error generating uuid: %v", err)
|
log.Printf("Error generating uuid: %v", err)
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
newSheet := models.Sheet{
|
newSheet := models.Sheet{
|
||||||
Uuid: *uuid,
|
Uuid: *uuid,
|
||||||
Title: file.Name(), // use filename as title
|
Title: strings.TrimSuffix(filepath.Base(filePath), ".pdf"), // use filename as title
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
FileSize: info.Size(),
|
FileSize: info.Size(),
|
||||||
FileHash: hash,
|
FileHash: hash,
|
||||||
@@ -93,10 +98,19 @@ func SyncSheets() error {
|
|||||||
}
|
}
|
||||||
if err := database.DB.Create(&newSheet).Error; err != nil {
|
if err := database.DB.Create(&newSheet).Error; err != nil {
|
||||||
log.Printf("Error creating new sheet for %s: %v", filePath, err)
|
log.Printf("Error creating new sheet for %s: %v", filePath, err)
|
||||||
|
} else {
|
||||||
|
numNewFiles++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if numFilesWithNewHash != 0 || numRenamedFiles != 0 || numNewFiles != 0 {
|
||||||
|
log.Printf("Library sync succesfully run.\nChanged hashes: %d, renamed files: %d, new files: %d", numFilesWithNewHash, numRenamedFiles, numNewFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
log.Printf("Sync took %s", time.Since(syncStartTime))
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FileHashFromUpload(file multipart.File) (uint64, error) {
|
func FileHashFromUpload(file multipart.File) (string, error) {
|
||||||
h := fnv.New64a()
|
h := fnv.New64a()
|
||||||
if _, err := io.Copy(h, file); err != nil {
|
if _, err := io.Copy(h, file); err != nil {
|
||||||
return 0, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.Sum64(), nil
|
return u64ToString(h.Sum64()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileHash(path string) (uint64, error) {
|
func FileHash(path string) (string, error) {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return "", err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
h := fnv.New64a()
|
h := fnv.New64a()
|
||||||
if _, err := io.Copy(h, f); err != nil {
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
return 0, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.Sum64(), nil
|
return u64ToString(h.Sum64()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func u64ToString(x uint64) string {
|
||||||
|
var b [8]byte
|
||||||
|
binary.LittleEndian.PutUint64(b[:], x)
|
||||||
|
return string(b[:])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user