Add scheduled rsync copy jobs
This commit is contained in:
@@ -34,8 +34,10 @@ func ValidateConfig(c Config) error {
|
||||
return fmt.Errorf("duplicate job id %q", job.ID)
|
||||
}
|
||||
jobs[job.ID] = struct{}{}
|
||||
if _, exists := repos[job.RepositoryID]; !exists {
|
||||
return fmt.Errorf("job %q references unknown repository %q", job.ID, job.RepositoryID)
|
||||
if job.Type != JobRsync {
|
||||
if _, exists := repos[job.RepositoryID]; !exists {
|
||||
return fmt.Errorf("job %q references unknown repository %q", job.ID, job.RepositoryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
notifications := make(map[string]struct{}, len(c.Notifications))
|
||||
@@ -90,11 +92,11 @@ func ValidateJob(j Job) error {
|
||||
if j.SchemaVersion != SchemaVersion || !idPattern.MatchString(j.ID) {
|
||||
return errors.New("invalid schemaVersion or id")
|
||||
}
|
||||
if strings.TrimSpace(j.Name) == "" || strings.TrimSpace(j.RepositoryID) == "" {
|
||||
return errors.New("name and repositoryId are required")
|
||||
if strings.TrimSpace(j.Name) == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
switch j.Type {
|
||||
case JobShare, JobAppdata, JobDocker, JobVM, JobFlash:
|
||||
case JobShare, JobAppdata, JobDocker, JobVM, JobFlash, JobRsync:
|
||||
default:
|
||||
return fmt.Errorf("unsupported job type %q", j.Type)
|
||||
}
|
||||
@@ -119,6 +121,17 @@ func ValidateJob(j Job) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, exclude := range j.Excludes {
|
||||
if strings.ContainsAny(exclude, "\x00\r\n") {
|
||||
return errors.New("exclude contains forbidden control characters")
|
||||
}
|
||||
}
|
||||
if j.Type == JobRsync {
|
||||
return validateRsyncJob(j)
|
||||
}
|
||||
if strings.TrimSpace(j.RepositoryID) == "" {
|
||||
return errors.New("repositoryId is required")
|
||||
}
|
||||
if j.Compression != "auto" && j.Compression != "off" && j.Compression != "max" {
|
||||
return errors.New("compression must be auto, off, or max")
|
||||
}
|
||||
@@ -134,14 +147,34 @@ func ValidateJob(j Job) error {
|
||||
if j.Retention.KeepWithinDays == 0 && j.Retention.Daily == 0 && j.Retention.Weekly == 0 && j.Retention.Monthly == 0 {
|
||||
return errors.New("retention must keep at least one age or calendar policy")
|
||||
}
|
||||
for _, exclude := range j.Excludes {
|
||||
if strings.ContainsAny(exclude, "\x00\r\n") {
|
||||
return errors.New("exclude contains forbidden control characters")
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRsyncJob(j Job) error {
|
||||
target := filepath.Clean(strings.TrimSpace(j.Rsync.Target))
|
||||
if !filepath.IsAbs(target) {
|
||||
return errors.New("rsync target must be absolute")
|
||||
}
|
||||
if !pathWithin(target, "/mnt/user") && !pathWithin(target, "/mnt/disks") && !pathWithin(target, "/mnt/remotes") {
|
||||
return errors.New("rsync target must be below /mnt/user, /mnt/disks, or /mnt/remotes")
|
||||
}
|
||||
for _, source := range j.Sources {
|
||||
if source.WorkloadID != "" || source.Path == "" {
|
||||
return errors.New("rsync jobs require filesystem paths")
|
||||
}
|
||||
cleanSource := filepath.Clean(source.Path)
|
||||
if pathWithin(target, cleanSource) || pathWithin(cleanSource, target) {
|
||||
return fmt.Errorf("rsync source %q and target %q must not contain each other", cleanSource, target)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathWithin(path, root string) bool {
|
||||
rel, err := filepath.Rel(filepath.Clean(root), filepath.Clean(path))
|
||||
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func ValidateRepository(r Repository) error {
|
||||
if r.SchemaVersion != SchemaVersion || !idPattern.MatchString(r.ID) {
|
||||
return errors.New("invalid schemaVersion or id")
|
||||
|
||||
Reference in New Issue
Block a user