Sybren A. Stüvel
02fac6a4df
Change the package base name of the Go code, from `git.blender.org/flamenco` to `projects.blender.org/studio/flamenco`. The old location, `git.blender.org`, has no longer been use since the [migration to Gitea][1]. The new package names now reflect the actual location where Flamenco is hosted. [1]: https://code.blender.org/2023/02/new-blender-development-infrastructure/
270 lines
8.1 KiB
Go
270 lines
8.1 KiB
Go
package worker
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* This file contains the commands in the "file-management" type group. */
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"projects.blender.org/studio/flamenco/pkg/api"
|
|
)
|
|
|
|
// cmdMoveDirectory executes the "move-directory" command.
|
|
// It moves directory 'src' to 'dest'; if 'dest' already exists, it's moved to 'dest-{timestamp}'.
|
|
func (ce *CommandExecutor) cmdMoveDirectory(ctx context.Context, logger zerolog.Logger, taskID string, cmd api.Command) error {
|
|
var src, dest string
|
|
var ok bool
|
|
|
|
if src, ok = cmdParameter[string](cmd, "src"); !ok || src == "" {
|
|
logger.Warn().Interface("command", cmd).Msg("missing 'src' parameter")
|
|
return NewParameterMissingError("src", cmd)
|
|
}
|
|
if dest, ok = cmdParameter[string](cmd, "dest"); !ok || dest == "" {
|
|
logger.Warn().Interface("command", cmd).Msg("missing 'dest' parameter")
|
|
return NewParameterMissingError("dest", cmd)
|
|
}
|
|
|
|
logger = logger.With().
|
|
Str("src", src).
|
|
Str("dest", dest).
|
|
Logger()
|
|
if !fileExists(src) {
|
|
logger.Warn().Msg("source path does not exist, not moving anything")
|
|
msg := fmt.Sprintf("%s: source path %q does not exist, not moving anything", cmd.Name, src)
|
|
if err := ce.listener.LogProduced(ctx, taskID, msg); err != nil {
|
|
return err
|
|
}
|
|
return NewParameterInvalidError("src", cmd, "path does not exist")
|
|
}
|
|
|
|
if fileExists(dest) {
|
|
backup, err := timestampedPath(dest)
|
|
if err != nil {
|
|
logger.Error().Err(err).Str("path", dest).Msg("unable to determine timestamp of directory")
|
|
return err
|
|
}
|
|
|
|
if fileExists(backup) {
|
|
logger.Debug().Str("backup", backup).Msg("backup destination also exists, finding one that does not")
|
|
backup, err = uniquePath(backup)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
logger.Info().
|
|
Str("toBackup", backup).
|
|
Msg("dest directory exists, moving to backup")
|
|
if err := ce.moveAndLog(ctx, taskID, cmd.Name, dest, backup); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// self._log.info("Moving %s to %s", src, dest)
|
|
// await self.worker.register_log(
|
|
// "%s: Moving %s to %s", self.command_name, src, dest
|
|
// )
|
|
// src.rename(dest)
|
|
logger.Info().Msg("moving directory")
|
|
return ce.moveAndLog(ctx, taskID, cmd.Name, src, dest)
|
|
}
|
|
|
|
// cmdCopyFiles executes the "copy-file" command.
|
|
// It takes an absolute source and destination file path,
|
|
// and copies the source file to its destination, if possible.
|
|
// Missing directories in destination path are created as needed.
|
|
// If the target path already exists, an error is returned. Destination will not be overwritten.
|
|
func (ce *CommandExecutor) cmdCopyFile(ctx context.Context, logger zerolog.Logger, taskID string, cmd api.Command) error {
|
|
var src, dest string
|
|
var ok bool
|
|
|
|
logger = logger.With().
|
|
Interface("command", cmd).
|
|
Str("src", src).
|
|
Str("dest", dest).
|
|
Logger()
|
|
|
|
if src, ok = cmdParameter[string](cmd, "src"); !ok || src == "" {
|
|
msg := "missing or empty 'src' parameter"
|
|
err := NewParameterMissingError("src", cmd)
|
|
return ce.errorLogProcess(ctx, logger, cmd, taskID, err, msg)
|
|
}
|
|
if !filepath.IsAbs(src) {
|
|
msg := fmt.Sprintf("source path %q is not absolute, not copying anything", src)
|
|
err := NewParameterInvalidError("src", cmd, "path is not absolute")
|
|
return ce.errorLogProcess(ctx, logger, cmd, taskID, err, msg)
|
|
}
|
|
if !fileExists(src) {
|
|
msg := fmt.Sprintf("source path %q does not exist, not copying anything", src)
|
|
err := NewParameterInvalidError("src", cmd, "path does not exist")
|
|
return ce.errorLogProcess(ctx, logger, cmd, taskID, err, msg)
|
|
}
|
|
|
|
if dest, ok = cmdParameter[string](cmd, "dest"); !ok || dest == "" {
|
|
msg := "missing or empty 'dest' parameter"
|
|
err := NewParameterMissingError("dest", cmd)
|
|
return ce.errorLogProcess(ctx, logger, cmd, taskID, err, msg)
|
|
}
|
|
if !filepath.IsAbs(dest) {
|
|
msg := fmt.Sprintf("destination path %q is not absolute, not copying anything", src)
|
|
err := NewParameterInvalidError("dest", cmd, "path is not absolute")
|
|
return ce.errorLogProcess(ctx, logger, cmd, taskID, err, msg)
|
|
}
|
|
if fileExists(dest) {
|
|
msg := fmt.Sprintf("destination path %q already exists, not copying anything", dest)
|
|
err := NewParameterInvalidError("dest", cmd, "path already exists")
|
|
return ce.errorLogProcess(ctx, logger, cmd, taskID, err, msg)
|
|
}
|
|
|
|
msg := fmt.Sprintf("copying %q to %q", src, dest)
|
|
if err := ce.errorLogProcess(ctx, logger, cmd, taskID, nil, msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
err, logMsg := fileCopy(src, dest)
|
|
return ce.errorLogProcess(ctx, logger, cmd, taskID, err, logMsg)
|
|
}
|
|
|
|
func (ce *CommandExecutor) errorLogProcess(ctx context.Context, logger zerolog.Logger, cmd api.Command, taskID string, err error, logMsg string) error {
|
|
msg := fmt.Sprintf("%s: %s", cmd.Name, logMsg)
|
|
|
|
if err != nil {
|
|
logger.Warn().Msg(msg)
|
|
} else {
|
|
logger.Info().Msg(msg)
|
|
}
|
|
|
|
if logErr := ce.listener.LogProduced(ctx, taskID, msg); logErr != nil {
|
|
return logErr
|
|
}
|
|
return err
|
|
}
|
|
|
|
// moveAndLog renames a file/directory from `src` to `dest`, and logs the moveAndLog.
|
|
// The other parameters are just for logging.
|
|
func (ce *CommandExecutor) moveAndLog(ctx context.Context, taskID, cmdName, src, dest string) error {
|
|
msg := fmt.Sprintf("%s: moving %q to %q", cmdName, src, dest)
|
|
if err := ce.listener.LogProduced(ctx, taskID, msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.Rename(src, dest); err != nil {
|
|
msg := fmt.Sprintf("%s: could not move %q to %q: %v", cmdName, src, dest, err)
|
|
if err := ce.listener.LogProduced(ctx, taskID, msg); err != nil {
|
|
return err
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func fileCopy(src, dest string) (error, string) {
|
|
src_file, err := os.Open(src)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("failed to open source file %q: %v", src, err)
|
|
return err, msg
|
|
}
|
|
defer src_file.Close()
|
|
|
|
src_file_stat, err := src_file.Stat()
|
|
if err != nil {
|
|
msg := fmt.Sprintf("failed to stat source file %q: %v", src, err)
|
|
return err, msg
|
|
}
|
|
|
|
if !src_file_stat.Mode().IsRegular() {
|
|
err := &os.PathError{Op: "stat", Path: src, Err: errors.New("Not a regular file")}
|
|
msg := fmt.Sprintf("invalid source file %q: %v", src, err)
|
|
return err, msg
|
|
}
|
|
|
|
dest_dirpath := filepath.Dir(dest)
|
|
if !fileExists(dest_dirpath) {
|
|
if err := os.MkdirAll(dest_dirpath, 0750); err != nil {
|
|
msg := fmt.Sprintf("failed to create directories %q: %v", dest_dirpath, err)
|
|
return err, msg
|
|
}
|
|
}
|
|
|
|
dest_file, err := os.Create(dest)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("failed to create destination file %q: %v", dest, err)
|
|
return err, msg
|
|
}
|
|
defer dest_file.Close()
|
|
|
|
if _, err := io.Copy(dest_file, src_file); err != nil {
|
|
msg := fmt.Sprintf("failed to copy %q to %q: %v", src, dest, err)
|
|
return err, msg
|
|
}
|
|
|
|
msg := fmt.Sprintf("copied %q to %q", src, dest)
|
|
return nil, msg
|
|
}
|
|
|
|
func fileExists(filename string) bool {
|
|
_, err := os.Stat(filename)
|
|
return !errors.Is(err, fs.ErrNotExist)
|
|
}
|
|
|
|
// timestampedPath returns the path with its modification time appended to the name.
|
|
func timestampedPath(filepath string) (string, error) {
|
|
stat, err := os.Stat(filepath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting mtime of %s: %w", filepath, err)
|
|
}
|
|
|
|
// Round away the milliseconds, as those aren't all that interesting.
|
|
// Uniqueness can ensured by calling unique_path() later.
|
|
mtime := stat.ModTime().Round(time.Second)
|
|
|
|
iso := mtime.Local().Format("2006-01-02_150405") // YYYY-MM-DD_HHMMSS
|
|
return fmt.Sprintf("%s-%s", filepath, iso), nil
|
|
}
|
|
|
|
// uniquePath returns the path, or if it exists, the path with a unique suffix.
|
|
func uniquePath(path string) (string, error) {
|
|
matches, err := filepath.Glob(path + "-*")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
suffixRe, err := regexp.Compile("-([0-9]+)$")
|
|
if err != nil {
|
|
return "", fmt.Errorf("compiling regular expression: %w", err)
|
|
}
|
|
|
|
var maxSuffix int64
|
|
for _, path := range matches {
|
|
matches := suffixRe.FindStringSubmatch(path)
|
|
if len(matches) < 2 {
|
|
continue
|
|
}
|
|
suffix := matches[1]
|
|
value, err := strconv.ParseInt(suffix, 10, 64)
|
|
if err != nil {
|
|
// Non-numeric suffixes are fine; they just don't count for this function.
|
|
continue
|
|
}
|
|
|
|
if value > maxSuffix {
|
|
maxSuffix = value
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("%s-%03d", path, maxSuffix+1), nil
|
|
}
|