import_file.go
package imp
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"skraak/db"
"skraak/utils"
"skraak/wav"
)
// ImportFileInput defines the input parameters for the import_file tool
type ImportFileInput struct {
DBPath string `json:"db_path"`
FilePath string `json:"file_path"`
DatasetID string `json:"dataset_id"`
LocationID string `json:"location_id"`
ClusterID string `json:"cluster_id"`
}
// ImportFileOutput defines the output structure for the import_file tool
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"`
}
// ImportFile imports a single WAV file into the database with duplicate detection
func ImportFile(
ctx context.Context,
input ImportFileInput,
) (ImportFileOutput, error) {
startTime := time.Now()
var output ImportFileOutput
// Phase 1: Validate file path
_, err := validateFilePath(input.FilePath)
if err != nil {
return output, fmt.Errorf("file validation failed: %w", err)
}
output.FileName = filepath.Base(input.FilePath)
// Phase 2: Validate database hierarchy
if err := validateHierarchyIDs(input.DatasetID, input.LocationID, input.ClusterID, db.ResolveDBPath(input.DBPath, "")); err != nil {
return output, fmt.Errorf("hierarchy validation failed: %w", err)
}
// Phase 3: Get location data and process file metadata
var locData *LocationData
var result *wav.FileProcessingResult
err = db.WithReadDB(db.ResolveDBPath(input.DBPath, ""), func(database *sql.DB) error {
var err error
locData, err = GetLocationData(database, input.LocationID)
if err != nil {
return fmt.Errorf("failed to get location data: %w", err)
}
result, err = wav.ProcessSingleFile(input.FilePath, locData.Latitude, locData.Longitude, locData.TimezoneID, true)
if err != nil {
return fmt.Errorf("file processing failed: %w", err)
}
return nil
})
if err != nil {
errMsg := err.Error()
output.Error = &errMsg
output.ProcessingTime = time.Since(startTime).String()
return output, err
}
// Populate output with extracted metadata
output.FileName = result.FileName
output.Hash = result.Hash
output.Duration = result.Duration
output.SampleRate = result.SampleRate
output.TimestampLocal = result.TimestampLocal
output.IsAudioMoth = result.IsAudioMoth
// Phase 4: Insert into database (includes EnsureClusterPath)
var fileID string
var isDuplicate bool
err = db.WithWriteTx(ctx, db.ResolveDBPath(input.DBPath, ""), "import_audio_file", func(_ *sql.DB, tx *db.LoggedTx) error {
var err error
fileID, isDuplicate, err = insertFileIntoDB(ctx, tx, result, input)
return err
})
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
}
// validateFilePath validates the file exists, is a regular file, is a WAV file, and is not empty
func validateFilePath(filePath string) (os.FileInfo, error) {
// Check file exists
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)
}
// Check it's a regular file
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("path is not a regular file: %s", filePath)
}
// Check extension is .wav (case-insensitive)
ext := strings.ToLower(filepath.Ext(filePath))
if ext != ".wav" {
return nil, fmt.Errorf("file must be a WAV file (got extension: %s)", ext)
}
// Check file is not empty
if info.Size() == 0 {
return nil, fmt.Errorf("file is empty: %s", filePath)
}
return info, nil
}
// insertFileIntoDB inserts a single file into the database using the provided mutator.
// Returns (fileID, isDuplicate, error)
func insertFileIntoDB(
ctx context.Context,
m Mutator,
result *wav.FileProcessingResult,
input ImportFileInput,
) (string, bool, error) {
// Ensure cluster path is set
if err := EnsureClusterPath(m, input.ClusterID, filepath.Dir(input.FilePath)); err != nil {
return "", false, fmt.Errorf("failed to set cluster path: %w", err)
}
// Check for duplicate hash
existingID, isDup, err := CheckDuplicateHash(m, result.Hash)
if err != nil {
return "", false, err
}
if isDup {
return existingID, true, nil
}
// Insert the new file record and related data
fileID, err := insertNewFileRecord(ctx, m, result, input)
if err != nil {
return "", false, err
}
return fileID, false, nil
}
// insertNewFileRecord inserts a file record, its dataset junction, and optional moth metadata.
func insertNewFileRecord(
ctx context.Context,
m Mutator,
result *wav.FileProcessingResult,
input ImportFileInput,
) (string, error) {
fileID, err := utils.GenerateLongID()
if err != nil {
return "", fmt.Errorf("ID generation failed: %w", err)
}
// Insert file record
_, err = m.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, input.LocationID,
result.TimestampLocal, input.ClusterID, result.Duration, result.SampleRate,
result.AstroData.SolarNight, result.AstroData.CivilNight, result.AstroData.MoonPhase,
)
if err != nil {
return "", fmt.Errorf("file insert failed: %w", err)
}
// Insert file_dataset junction
_, err = m.ExecContext(ctx, `
INSERT INTO file_dataset (file_id, dataset_id, created_at, last_modified)
VALUES (?, ?, now(), now())
`, fileID, input.DatasetID)
if err != nil {
return "", fmt.Errorf("file_dataset insert failed: %w", err)
}
// If AudioMoth, insert moth_metadata
if result.IsAudioMoth && result.MothData != nil {
_, err = m.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 "", fmt.Errorf("moth_metadata insert failed: %w", err)
}
}
return fileID, nil
}