package tools
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"skraak/db"
"skraak/utils"
)
type ImportFileInput struct {
FilePath string `json:"file_path"`
DatasetID string `json:"dataset_id"`
LocationID string `json:"location_id"`
ClusterID string `json:"cluster_id"`
}
type ImportFileOutput struct {
FileID string `json:"file_id"`
FileName string `json:"file_name"`
Hash string `json:"hash"`
Duration float64 `json:"duration_seconds"`
SampleRate int `json:"sample_rate"`
TimestampLocal time.Time `json:"timestamp_local"`
IsAudioMoth bool `json:"is_audiomoth"`
IsDuplicate bool `json:"is_duplicate"`
ProcessingTime string `json:"processing_time"`
Error *string `json:"error,omitempty"`
}
func ImportFile(
ctx context.Context,
input ImportFileInput,
) (ImportFileOutput, error) {
startTime := time.Now()
var output ImportFileOutput
_, err := validateFilePath(input.FilePath)
if err != nil {
return output, fmt.Errorf("file validation failed: %w", err)
}
output.FileName = filepath.Base(input.FilePath)
if err := validateHierarchyIDs(input.DatasetID, input.LocationID, input.ClusterID, dbPath); err != nil {
return output, fmt.Errorf("hierarchy validation failed: %w", err)
}
database, err := db.OpenWriteableDB(dbPath)
if err != nil {
return output, fmt.Errorf("database connection failed: %w", err)
}
defer database.Close()
locData, err := utils.GetLocationData(database, input.LocationID)
if err != nil {
return output, fmt.Errorf("failed to get location data: %w", err)
}
result, err := utils.ProcessSingleFile(input.FilePath, locData.Latitude, locData.Longitude, locData.TimezoneID, true)
if err != nil {
errMsg := err.Error()
output.Error = &errMsg
output.ProcessingTime = time.Since(startTime).String()
return output, fmt.Errorf("file processing failed: %w", err)
}
output.FileName = result.FileName
output.Hash = result.Hash
output.Duration = result.Duration
output.SampleRate = result.SampleRate
output.TimestampLocal = result.TimestampLocal
output.IsAudioMoth = result.IsAudioMoth
if err := utils.EnsureClusterPath(database, input.ClusterID, filepath.Dir(input.FilePath)); err != nil {
return output, fmt.Errorf("failed to set cluster path: %w", err)
}
fileID, isDuplicate, err := insertFileIntoDB(ctx, database, result, input.DatasetID, input.ClusterID, input.LocationID)
if err != nil {
errMsg := err.Error()
output.Error = &errMsg
output.ProcessingTime = time.Since(startTime).String()
return output, fmt.Errorf("database insertion failed: %w", err)
}
output.FileID = fileID
output.IsDuplicate = isDuplicate
output.ProcessingTime = time.Since(startTime).String()
return output, nil
}
func validateFilePath(filePath string) (os.FileInfo, error) {
info, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("file does not exist: %s", filePath)
}
return nil, fmt.Errorf("cannot access file: %w", err)
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("path is not a regular file: %s", filePath)
}
ext := strings.ToLower(filepath.Ext(filePath))
if ext != ".wav" {
return nil, fmt.Errorf("file must be a WAV file (got extension: %s)", ext)
}
if info.Size() == 0 {
return nil, fmt.Errorf("file is empty: %s", filePath)
}
return info, nil
}
func insertFileIntoDB(
ctx context.Context,
database *sql.DB,
result *utils.FileProcessingResult,
datasetID, clusterID, locationID string,
) (string, bool, error) {
tx, err := db.BeginLoggedTx(ctx, database, "import_audio_file")
if err != nil {
return "", false, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
existingID, isDup, err := utils.CheckDuplicateHash(tx, result.Hash)
if err != nil {
return "", false, err
}
if isDup {
return existingID, true, nil
}
fileID, err := utils.GenerateLongID()
if err != nil {
return "", false, fmt.Errorf("ID generation failed: %w", err)
}
_, err = tx.ExecContext(ctx, `
INSERT INTO file (
id, file_name, xxh64_hash, location_id, timestamp_local,
cluster_id, duration, sample_rate, maybe_solar_night, maybe_civil_night,
moon_phase, created_at, last_modified, active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now(), true)
`,
fileID, result.FileName, result.Hash, locationID,
result.TimestampLocal, clusterID, result.Duration, result.SampleRate,
result.AstroData.SolarNight, result.AstroData.CivilNight, result.AstroData.MoonPhase,
)
if err != nil {
return "", false, fmt.Errorf("file insert failed: %w", err)
}
_, err = tx.ExecContext(ctx, `
INSERT INTO file_dataset (file_id, dataset_id, created_at, last_modified)
VALUES (?, ?, now(), now())
`, fileID, datasetID)
if err != nil {
return "", false, fmt.Errorf("file_dataset insert failed: %w", err)
}
if result.IsAudioMoth && result.MothData != nil {
_, err = tx.ExecContext(ctx, `
INSERT INTO moth_metadata (
file_id, timestamp, recorder_id, gain, battery_v, temp_c,
created_at, last_modified, active
) VALUES (?, ?, ?, ?, ?, ?, now(), now(), true)
`,
fileID,
result.MothData.Timestamp,
&result.MothData.RecorderID,
&result.MothData.Gain,
&result.MothData.BatteryV,
&result.MothData.TempC,
)
if err != nil {
return "", false, fmt.Errorf("moth_metadata insert failed: %w", err)
}
}
if err = tx.Commit(); err != nil {
return "", false, fmt.Errorf("transaction commit failed: %w", err)
}
return fileID, false, nil
}