flamenco/internal/worker/command_file_mgmt.go
Sybren A. Stüvel a9bec98fcd Fix linter warnings
Fix most linter warnings reported by 'staticcheck'. This doesn't fix all
of them, some unused functions are still there, and some generated code
also still triggers some warnings. Most issues are fixed, though.

No functional changes, except for the captialisation of some error
messages.
2024-12-01 14:49:25 +01: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
}
logMsg, err := 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) (string, error) {
src_file, err := os.Open(src)
if err != nil {
msg := fmt.Sprintf("failed to open source file %q: %v", src, err)
return msg, err
}
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 msg, err
}
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 msg, err
}
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 msg, err
}
}
dest_file, err := os.Create(dest)
if err != nil {
msg := fmt.Sprintf("failed to create destination file %q: %v", dest, err)
return msg, err
}
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 msg, err
}
msg := fmt.Sprintf("copied %q to %q", src, dest)
return msg, nil
}
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
}