mirror of
https://git.sr.ht/~phw/scotty
synced 2025-06-01 19:38:34 +02:00
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.
198 lines
5.3 KiB
Go
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)))
|
|
}
|