adds dedup command
This commit is contained in:
121
db/commands/dedup.go
Normal file
121
db/commands/dedup.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dedup(app *pocketbase.PocketBase) *cobra.Command {
|
||||||
|
var dryRun bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "dedup",
|
||||||
|
Short: "Deduplicate trails by all matching fields",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
records, err := app.FindAllRecords("trails")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to fetch trails: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// group by composite key
|
||||||
|
trailsByKey := make(map[string][]*core.Record)
|
||||||
|
for _, r := range records {
|
||||||
|
key := makeKey(r)
|
||||||
|
trailsByKey[key] = append(trailsByKey[key], r)
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicates []*core.Record
|
||||||
|
for _, recs := range trailsByKey {
|
||||||
|
if len(recs) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by created date ascending
|
||||||
|
sort.Slice(recs, func(i, j int) bool {
|
||||||
|
return recs[i].GetDateTime("created").Time().Before(recs[j].GetDateTime("created").Time())
|
||||||
|
})
|
||||||
|
|
||||||
|
original := recs[0]
|
||||||
|
dupes := recs[1:]
|
||||||
|
|
||||||
|
// print header row for original
|
||||||
|
// print original as header
|
||||||
|
fmt.Printf("\nOriginal: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
|
||||||
|
original.Id,
|
||||||
|
original.GetString("name"),
|
||||||
|
original.GetFloat("distance"),
|
||||||
|
original.GetFloat("elevation_gain"),
|
||||||
|
original.GetFloat("elevation_loss"),
|
||||||
|
original.GetFloat("lat"),
|
||||||
|
original.GetFloat("lon"),
|
||||||
|
original.GetFloat("duration"),
|
||||||
|
original.GetString("location"),
|
||||||
|
original.GetString("category"),
|
||||||
|
original.GetString("author"),
|
||||||
|
original.GetDateTime("created"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// print duplicates indented
|
||||||
|
for _, d := range dupes {
|
||||||
|
fmt.Printf(" Duplicate: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
|
||||||
|
d.Id,
|
||||||
|
d.GetString("name"),
|
||||||
|
d.GetFloat("distance"),
|
||||||
|
d.GetFloat("elevation_gain"),
|
||||||
|
d.GetFloat("elevation_loss"),
|
||||||
|
d.GetFloat("lat"),
|
||||||
|
d.GetFloat("lon"),
|
||||||
|
d.GetFloat("duration"),
|
||||||
|
d.GetString("location"),
|
||||||
|
d.GetString("category"),
|
||||||
|
d.GetString("author"),
|
||||||
|
d.GetDateTime("created"),
|
||||||
|
)
|
||||||
|
duplicates = append(duplicates, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("\n[Dry Run] Found %d duplicates (no deletions performed)\n", len(duplicates))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete duplicates
|
||||||
|
for _, d := range duplicates {
|
||||||
|
if err := app.Delete(d); err != nil {
|
||||||
|
fmt.Printf("Failed to delete duplicate %s: %v\n", d.Id, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Deleted duplicate %s\n", d.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show duplicates without deleting them")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeKey creates a composite key string for duplicate detection
|
||||||
|
func makeKey(r *core.Record) string {
|
||||||
|
data := fmt.Sprintf("%s|%f|%f|%f|%f|%f|%f|%s|%s|%s",
|
||||||
|
r.GetString("name"),
|
||||||
|
r.GetFloat("distance"),
|
||||||
|
r.GetFloat("elevation_gain"),
|
||||||
|
r.GetFloat("elevation_loss"),
|
||||||
|
r.GetFloat("lat"),
|
||||||
|
r.GetFloat("lon"),
|
||||||
|
r.GetFloat("duration"),
|
||||||
|
r.GetString("location"),
|
||||||
|
r.GetString("category"),
|
||||||
|
r.GetString("author"),
|
||||||
|
)
|
||||||
|
h := sha1.Sum([]byte(data))
|
||||||
|
return fmt.Sprintf("%x", h)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/tools/security"
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
|
"pocketbase/commands"
|
||||||
"pocketbase/federation"
|
"pocketbase/federation"
|
||||||
"pocketbase/integrations/komoot"
|
"pocketbase/integrations/komoot"
|
||||||
"pocketbase/integrations/strava"
|
"pocketbase/integrations/strava"
|
||||||
@@ -73,6 +74,8 @@ func main() {
|
|||||||
registerMigrations(app)
|
registerMigrations(app)
|
||||||
setupEventHandlers(app, client)
|
setupEventHandlers(app, client)
|
||||||
|
|
||||||
|
setupCommands(app)
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
if err := app.Start(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -140,6 +143,10 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
|||||||
app.OnBootstrap().BindFunc(onBootstrapHandler())
|
app.OnBootstrap().BindFunc(onBootstrapHandler())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupCommands(app *pocketbase.PocketBase) {
|
||||||
|
app.RootCmd.AddCommand(commands.Dedup(app))
|
||||||
|
}
|
||||||
|
|
||||||
func sanitizeHTML() func(e *core.RecordRequestEvent) error {
|
func sanitizeHTML() func(e *core.RecordRequestEvent) error {
|
||||||
return func(e *core.RecordRequestEvent) error {
|
return func(e *core.RecordRequestEvent) error {
|
||||||
fieldsToSanitize := map[string][]string{
|
fieldsToSanitize := map[string][]string{
|
||||||
|
|||||||
Reference in New Issue
Block a user