2022-12-10 23:01:16 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"context"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2023-07-10 00:28:00 +00:00
|
|
|
"gopkg.in/irc.v4"
|
|
|
|
|
2022-12-10 23:01:16 +00:00
|
|
|
"git.sr.ht/~emersion/soju/database"
|
|
|
|
"git.sr.ht/~emersion/soju/msgstore"
|
2023-02-17 13:35:09 +00:00
|
|
|
"git.sr.ht/~emersion/soju/msgstore/znclog"
|
2022-12-10 23:01:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const usage = `usage: migrate-logs <source logs> <destination database>
|
|
|
|
|
|
|
|
Migrates existing Soju logs stored on disk to a Soju database. Database is specified
|
|
|
|
in the format of "driver:source" where driver is sqlite3 or postgres and source
|
|
|
|
is the string that would be in the Soju config file.
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
|
|
|
-help Show this help message
|
|
|
|
`
|
|
|
|
|
|
|
|
var logRoot string
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
flag.Usage = func() {
|
|
|
|
fmt.Fprint(flag.CommandLine.Output(), usage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func migrateNetwork(ctx context.Context, db database.Database, user *database.User, network *database.Network) error {
|
|
|
|
log.Printf("Migrating logs for network: %s\n", network.Name)
|
|
|
|
|
|
|
|
rootPath := filepath.Join(logRoot, msgstore.EscapeFilename(user.Username), msgstore.EscapeFilename(network.GetName()))
|
|
|
|
root, err := os.Open(rootPath)
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to open network folder: %s", rootPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// The returned targets are escaped, and there is no way to un-escape
|
|
|
|
// TODO: switch to ReadDir (Go 1.16+)
|
|
|
|
targets, err := root.Readdirnames(0)
|
|
|
|
root.Close()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to read network folder: %s", rootPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, target := range targets {
|
|
|
|
log.Printf("Migrating logs for target: %s\n", target)
|
|
|
|
|
|
|
|
// target is already escaped here
|
|
|
|
targetPath := filepath.Join(rootPath, target)
|
|
|
|
targetDir, err := os.Open(targetPath)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to open target folder: %s", targetPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
entryNames, err := targetDir.Readdirnames(0)
|
|
|
|
targetDir.Close()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to read target folder: %s", targetPath)
|
|
|
|
}
|
|
|
|
sort.Strings(entryNames)
|
|
|
|
|
|
|
|
for _, entryName := range entryNames {
|
|
|
|
entryPath := filepath.Join(targetPath, entryName)
|
|
|
|
|
|
|
|
var year, month, day int
|
|
|
|
_, err := fmt.Sscanf(entryName, "%04d-%02d-%02d.log", &year, &month, &day)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid entry name: %s", entryName)
|
|
|
|
}
|
|
|
|
ref := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
|
|
|
|
|
|
|
|
entry, err := os.Open(entryPath)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to open entry: %s", entryPath)
|
|
|
|
}
|
|
|
|
sc := bufio.NewScanner(entry)
|
2023-07-10 00:28:00 +00:00
|
|
|
var msgs []*irc.Message
|
2022-12-10 23:01:16 +00:00
|
|
|
for sc.Scan() {
|
2023-02-17 13:35:09 +00:00
|
|
|
msg, _, err := znclog.UnmarshalLine(sc.Text(), user, network, target, ref, true)
|
2022-12-10 23:01:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to parse entry: %s: %s", entryPath, sc.Text())
|
|
|
|
} else if msg == nil {
|
|
|
|
continue
|
|
|
|
}
|
2023-07-10 00:28:00 +00:00
|
|
|
msgs = append(msgs, msg)
|
2022-12-10 23:01:16 +00:00
|
|
|
}
|
|
|
|
if sc.Err() != nil {
|
|
|
|
return fmt.Errorf("unable to parse entry: %s: %v", entryPath, sc.Err())
|
|
|
|
}
|
2023-07-10 00:28:00 +00:00
|
|
|
_, err = db.StoreMessages(ctx, network.ID, target, msgs)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to store messages: %s: %s: %v", entryPath, sc.Text(), err)
|
|
|
|
}
|
2022-12-10 23:01:16 +00:00
|
|
|
entry.Close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
logRoot = flag.Arg(0)
|
|
|
|
dbParams := strings.Split(flag.Arg(1), ":")
|
|
|
|
|
|
|
|
if len(dbParams) != 2 {
|
|
|
|
log.Fatalf("database not properly specified: %s", flag.Arg(1))
|
|
|
|
}
|
|
|
|
|
|
|
|
db, err := database.Open(dbParams[0], dbParams[1])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("failed to open database: %v", err)
|
|
|
|
}
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
users, err := db.ListUsers(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("unable to get users: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, user := range users {
|
|
|
|
log.Printf("Migrating logs for user: %s\n", user.Username)
|
|
|
|
|
|
|
|
networks, err := db.ListNetworks(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("unable to get networks for user: #%d %s", user.ID, user.Username)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, network := range networks {
|
|
|
|
if err := migrateNetwork(ctx, db, &user, &network); err != nil {
|
|
|
|
log.Fatalf("migrating %v: %v", network.Name, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|