Created
August 2, 2024 11:30
-
-
Save mitranim/1e061674159018895dd5a6869d2ce213 to your computer and use it in GitHub Desktop.
elden_ring_backup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Simple CLI tool for backing up Elden Ring saves. Makes | |
backups periodically. Interval is configurable, see | |
constants below. Avoids redundant backups by checking | |
file modification timestamps. | |
Requires Go. To install Go on Windows, use any of the following, | |
depending on your preference: | |
* Official download: https://go.dev/dl/ | |
* `scoop install golang` (if you use Scoop) | |
* `choco install golang` (if you use Chocolatey) | |
Before running, specify the source and destination path | |
by editing the constants `SRC_DIR` and `TAR_DIR` below. | |
To run, open a terminal, navigate to this directory, then: | |
go run . | |
*/ | |
package main | |
import ( | |
"errors" | |
"fmt" | |
"io" | |
"io/fs" | |
"log" | |
"os" | |
"path/filepath" | |
"strconv" | |
"strings" | |
"time" | |
"github.com/mitranim/gg" | |
) | |
// Edit the path to match your username. | |
const SRC_DIR = `C:\Users\mitranim\AppData\Roaming\EldenRing` | |
const TAR_DIR = `C:\Users\mitranim\Downloads\elden_ring_backups` | |
const DELAY_UP_TO_DATE = time.Hour | |
const DELAY_SUCCESS = time.Minute * 15 | |
const DELAY_ERROR = time.Minute * 5 | |
func main() { | |
for { | |
err := backup(SRC_DIR, TAR_DIR) | |
if gg.AnyIs[ErrUpToDate](err) { | |
log.Printf(`%v, retry after %v`, err, DELAY_UP_TO_DATE) | |
time.Sleep(DELAY_UP_TO_DATE) | |
continue | |
} | |
if err != nil { | |
log.Printf(`error, retry after %v: %+v`, DELAY_ERROR, err) | |
time.Sleep(DELAY_ERROR) | |
continue | |
} | |
log.Printf(`success, retry after %v`, DELAY_SUCCESS) | |
time.Sleep(DELAY_SUCCESS) | |
continue | |
} | |
} | |
type ErrUpToDate string | |
func (self ErrUpToDate) Error() string { return string(self) } | |
func backup(srcPath, tarDir string) (out error) { | |
defer gg.Rec(&out) | |
nextName, prevName := indexedName(tarDir, filepath.Base(srcPath)) | |
if prevName != `` { | |
nextTime := maxModTime(srcPath) | |
prevTime := maxModTime(filepath.Join(tarDir, prevName)) | |
if prevTime.After(nextTime) { | |
return ErrUpToDate(fmt.Sprintf(`latest backup %q is up to date`, prevName)) | |
} | |
} | |
copyRecursive(srcPath, tarDir, nextName) | |
return | |
} | |
func copyRecursive(srcPath, tarDir, tarName string) { | |
srcInfo := gg.Try1(os.Stat(srcPath)) | |
tarPath := filepath.Join(tarDir, tarName) | |
if srcInfo.IsDir() { | |
copyDirRecursive(srcPath, tarPath) | |
} else { | |
gg.Try(os.MkdirAll(tarDir, os.ModePerm)) | |
copyFile(srcPath, tarPath) | |
} | |
} | |
func copyDirRecursive(srcDir, tarDir string) { | |
for _, name := range readDir(srcDir) { | |
copyRecursive(filepath.Join(srcDir, name), tarDir, name) | |
} | |
} | |
func copyFile(srcPath, tarPath string) { | |
src := gg.Try1(os.OpenFile(srcPath, os.O_RDONLY, os.ModePerm)) | |
defer src.Close() // Ignore error. | |
out := gg.Try1(os.Create(tarPath)) | |
defer gg.Close(out) // Do not ignore error. | |
gg.Try1(io.Copy(out, src)) | |
} | |
func indexedName(dirPath, srcName string) (string, string) { | |
srcName, srcExt := fileNameSplit(srcName) | |
tarNames := readDir(dirPath) | |
var prevName string | |
var prevInd uint64 | |
for _, tarBaseName := range tarNames { | |
tarName, tarInd, tarExt := indexedNameDecode(tarBaseName) | |
if tarName == srcName && tarExt == srcExt && tarInd > prevInd { | |
prevName = tarBaseName | |
prevInd = tarInd | |
} | |
} | |
tarNameSet := gg.SetOf(tarNames...) | |
for { | |
out := indexedNameEncode(srcName, prevInd, srcExt) | |
if tarNameSet.Has(out) { | |
prevInd++ | |
continue | |
} | |
return out, prevName | |
} | |
} | |
const indexWidth = 4 | |
const indexRadix = 10 | |
func indexEncode(src uint64) string { | |
out := strconv.FormatUint(src, indexRadix) | |
missing := indexWidth - len(out) | |
if missing > 0 { | |
return repeatByte('0', missing) + out | |
} | |
return out | |
} | |
func repeatByte(src byte, count int) (_ string) { | |
if count > 0 { | |
buf := make([]byte, count) | |
for ind := range buf { | |
buf[ind] = src | |
} | |
return string(buf) | |
} | |
return | |
} | |
func indexedNameDecode(src string) (string, uint64, string) { | |
name, ext := fileNameSplit(src) | |
pre, suf, ok := strings.Cut(name, ` `) | |
if !ok { | |
return name, 0, ext | |
} | |
ind, err := strconv.ParseUint(suf, indexRadix, 64) | |
if err != nil { | |
return name, 0, ext | |
} | |
return pre, ind, ext | |
} | |
func indexedNameEncode(pre string, ind uint64, ext string) string { | |
return pre + ` ` + indexEncode(ind) + ext | |
} | |
// Workaround for bugs in `filepath.Ext`. | |
func fileNameSplit(src string) (string, string) { | |
name := filepath.Base(src) | |
ext := filepath.Ext(name) | |
base := strings.TrimSuffix(name, ext) | |
if base == `` { | |
return ext, `` | |
} | |
return base, ext | |
} | |
// Difference from `os.ReadDir`: returns strings, not `[]os.FileInfo`. | |
func readDir(srcPath string) []string { | |
defer gg.SkipOnly(isErrFileNotFound) | |
src := gg.Try1(os.OpenFile(srcPath, os.O_RDONLY, os.ModePerm)) | |
defer src.Close() | |
return gg.Try1(src.Readdirnames(-1)) | |
} | |
func isErrFileNotFound(err error) bool { return errors.Is(err, os.ErrNotExist) } | |
/* | |
Note: despite its name, `filepath.WalkDir` also supports walking a single file. | |
This function should work for both directory backups and single file backups. | |
*/ | |
func maxModTime(src string) (out time.Time) { | |
gg.Try(filepath.WalkDir( | |
src, | |
func(_ string, src fs.DirEntry, _ error) error { | |
if src == nil { | |
return nil | |
} | |
info, _ := src.Info() | |
if info == nil { | |
return nil | |
} | |
val := info.ModTime() | |
if val.After(out) { | |
out = val | |
} | |
return nil | |
}, | |
)) | |
return | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module elden_ring_backup | |
go 1.22.5 | |
require github.com/mitranim/gg v0.1.23 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
github.com/mitranim/gg v0.1.23 h1:U91GBI6qCG7+4VrWVg/Fm8OYkVAI6/1sE6zwVzrEDTw= | |
github.com/mitranim/gg v0.1.23/go.mod h1:x2V+nJJOpeMl/XEoHou9zlTvFxYAcGOCqOAKpVkF0Yc= |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Superseded by https://github.com/mitranim/backup.