Flamenco/internal/worker/command_file_mgmt.go
Sybren A. Stüvel 02fac6a4df Change Go package name from git.blender.org to projects.blender.org
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/
2023-08-01 12:42:31 +02:00

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
}