scotty/internal/cli/transfer.go
Philipp Wolfer 3b545a0fd6
Prepare using a context for export / import
This will allow cancelling the export if the import fails
before the export finished.

For now the context isn't passed on to the actual export functions,
hence there is not yet any cancellation happening.
2025-05-22 11:51:51 +02:00

198 lines
5.3 KiB
Go

/*
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/
package cli
import (
"context"
"errors"
"fmt"
"strconv"
"sync"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uploadedlobster.com/scotty/internal/backends"
"go.uploadedlobster.com/scotty/internal/config"
"go.uploadedlobster.com/scotty/internal/i18n"
"go.uploadedlobster.com/scotty/internal/models"
"go.uploadedlobster.com/scotty/internal/storage"
)
func NewTransferCmd[
E models.Backend,
I models.ImportBackend,
R models.ListensResult | models.LovesResult,
](
cmd *cobra.Command,
db *storage.Database,
entity models.Entity,
source string,
target string,
) (TransferCmd[E, I, R], error) {
c := TransferCmd[E, I, R]{
cmd: cmd,
db: db,
entity: entity,
}
err := c.resolveBackends(source, target)
if err != nil {
return c, err
}
return c, nil
}
type TransferCmd[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult] struct {
cmd *cobra.Command
db *storage.Database
entity models.Entity
sourceName string
targetName string
ExpBackend E
ImpBackend I
}
func (c *TransferCmd[E, I, R]) resolveBackends(source string, target string) error {
sourceConfig, err := config.GetService(source)
cobra.CheckErr(err)
targetConfig, err := config.GetService(target)
cobra.CheckErr(err)
// Initialize backends
expBackend, err := backends.ResolveBackend[E](sourceConfig)
if err != nil {
return err
}
impBackend, err := backends.ResolveBackend[I](targetConfig)
if err != nil {
return err
}
c.sourceName = sourceConfig.Name
c.targetName = targetConfig.Name
c.ExpBackend = expBackend
c.ImpBackend = impBackend
return nil
}
func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp backends.ImportProcessor[R]) error {
fmt.Println(i18n.Tr("Transferring %s from %s to %s…", c.entity, c.sourceName, c.targetName))
// Authenticate backends, if needed
config := viper.GetViper()
_, err := backends.Authenticate(c.sourceName, c.ExpBackend, *c.db, config)
if err != nil {
return err
}
_, err = backends.Authenticate(c.targetName, c.ImpBackend, *c.db, config)
if err != nil {
return err
}
// Read timestamp
timestamp, err := c.timestamp()
if err != nil {
return err
}
printTimestamp("From timestamp: %v (%v)", timestamp)
// Prepare progress bars
progressChan := make(chan models.TransferProgress)
progress := setupProgressBars(progressChan)
ctx, cancel := context.WithCancel(context.Background())
wg := &sync.WaitGroup{}
// Export from source
exportChan := make(chan R, 1000)
go exp.Process(ctx, wg, timestamp, exportChan, progressChan)
// Import into target
resultChan := make(chan models.ImportResult)
go imp.Process(ctx, wg, exportChan, resultChan, progressChan)
result := <-resultChan
// Once import is done, the context can be cancelled
cancel()
// Wait for all goroutines to finish
wg.Wait()
progress.close()
// Update timestamp
err = c.updateTimestamp(&result, timestamp)
if err != nil {
return err
}
fmt.Println(i18n.Tr("Imported %v of %v %s into %v.",
result.ImportCount, result.TotalCount, c.entity, c.targetName))
if result.Error != nil {
printTimestamp("Import failed, last reported timestamp was %v (%s)", result.LastTimestamp)
return result.Error
}
// Print errors
if len(result.ImportLog) > 0 {
fmt.Println()
fmt.Println(i18n.Tr("Import log:"))
for _, entry := range result.ImportLog {
fmt.Println(i18n.Tr("%v: %v", entry.Type, entry.Message))
}
}
return nil
}
func (c *TransferCmd[E, I, R]) timestamp() (time.Time, error) {
flagValue, err := c.cmd.Flags().GetString("timestamp")
if err != nil {
return time.Time{}, err
}
// No timestamp given, read from database
if flagValue == "" {
timestamp, err := c.db.GetImportTimestamp(c.sourceName, c.targetName, c.entity)
return timestamp, err
}
// Try using given value as a Unix timestamp
if timestamp, err := strconv.ParseInt(flagValue, 10, 64); err == nil {
return time.Unix(timestamp, 0), nil
}
// Try to parse datetime string
for _, format := range []string{time.DateTime, time.RFC3339} {
if t, err := time.Parse(format, flagValue); err == nil {
return t, nil
}
}
return time.Time{}, errors.New(i18n.Tr("invalid timestamp string \"%v\"", flagValue))
}
func (c *TransferCmd[E, I, R]) updateTimestamp(result *models.ImportResult, oldTimestamp time.Time) error {
if oldTimestamp.After(result.LastTimestamp) {
result.LastTimestamp = oldTimestamp
}
printTimestamp("Latest timestamp: %v (%v)", result.LastTimestamp)
err := c.db.SetImportTimestamp(c.sourceName, c.targetName, c.entity, result.LastTimestamp)
return err
}
func printTimestamp(s string, t time.Time) {
fmt.Println(i18n.Tr(s, t, strconv.FormatInt(t.Unix(), 10)))
}