Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b3cb9e393 | |||
| 7c5736b935 | |||
| 8f36d4e824 |
@@ -10,7 +10,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" \
|
|||||||
|
|
||||||
FROM alpine:3.19
|
FROM alpine:3.19
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata ca-certificates
|
RUN apk add --no-cache tzdata ca-certificates rsync
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /out/jukebox .
|
COPY --from=builder /out/jukebox .
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ func triggerAutoCopy(cp *copier.Copier, cfg *config.Config, info disk.DiskInfo,
|
|||||||
DiskID: info.DiskID,
|
DiskID: info.DiskID,
|
||||||
MountPath: info.MountPath,
|
MountPath: info.MountPath,
|
||||||
MediaPath: mediaPath,
|
MediaPath: mediaPath,
|
||||||
|
DestFolder: cfg.DestFolder,
|
||||||
EnabledSources: sources,
|
EnabledSources: sources,
|
||||||
ReserveFreeGB: cfg.ReserveFreeGB,
|
ReserveFreeGB: cfg.ReserveFreeGB,
|
||||||
OverwriteMode: cfg.OverwriteMode,
|
OverwriteMode: cfg.OverwriteMode,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func (s *Server) handleCopyStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
DiskID: diskInfo.DiskID,
|
DiskID: diskInfo.DiskID,
|
||||||
MountPath: diskInfo.MountPath,
|
MountPath: diskInfo.MountPath,
|
||||||
MediaPath: s.deps.MediaPath,
|
MediaPath: s.deps.MediaPath,
|
||||||
|
DestFolder: cfg.DestFolder,
|
||||||
EnabledSources: enabledSources,
|
EnabledSources: enabledSources,
|
||||||
ReserveFreeGB: cfg.ReserveFreeGB,
|
ReserveFreeGB: cfg.ReserveFreeGB,
|
||||||
OverwriteMode: cfg.OverwriteMode,
|
OverwriteMode: cfg.OverwriteMode,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type SourceFolder struct {
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ReserveFreeGB float64 `json:"reserve_free_gb"`
|
ReserveFreeGB float64 `json:"reserve_free_gb"`
|
||||||
|
DestFolder string `json:"dest_folder"`
|
||||||
Sources []SourceFolder `json:"sources"`
|
Sources []SourceFolder `json:"sources"`
|
||||||
OverwriteMode OverwriteMode `json:"overwrite_mode"`
|
OverwriteMode OverwriteMode `json:"overwrite_mode"`
|
||||||
FileSelectMode FileSelectMode `json:"file_select_mode"`
|
FileSelectMode FileSelectMode `json:"file_select_mode"`
|
||||||
@@ -34,6 +35,7 @@ type Config struct {
|
|||||||
func defaults() Config {
|
func defaults() Config {
|
||||||
return Config{
|
return Config{
|
||||||
ReserveFreeGB: 2.0,
|
ReserveFreeGB: 2.0,
|
||||||
|
DestFolder: "media",
|
||||||
OverwriteMode: OverwriteSkip,
|
OverwriteMode: OverwriteSkip,
|
||||||
FileSelectMode: SelectNew,
|
FileSelectMode: SelectNew,
|
||||||
AutoCopy: false,
|
AutoCopy: false,
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"math/rand/v2"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"jukebox_maker/internal/config"
|
"jukebox_maker/internal/config"
|
||||||
"jukebox_maker/internal/db"
|
"jukebox_maker/internal/db"
|
||||||
@@ -19,6 +21,7 @@ type Options struct {
|
|||||||
DiskID string
|
DiskID string
|
||||||
MountPath string
|
MountPath string
|
||||||
MediaPath string
|
MediaPath string
|
||||||
|
DestFolder string // subfolder on disk, default "media"
|
||||||
EnabledSources []string
|
EnabledSources []string
|
||||||
ReserveFreeGB float64
|
ReserveFreeGB float64
|
||||||
OverwriteMode config.OverwriteMode
|
OverwriteMode config.OverwriteMode
|
||||||
@@ -39,7 +42,6 @@ func New(tasks *task.Store) *Copier {
|
|||||||
return &Copier{tasks: tasks}
|
return &Copier{tasks: tasks}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDB replaces the active disk database (called when a disk connects or disconnects).
|
|
||||||
func (c *Copier) SetDB(d *db.DB) {
|
func (c *Copier) SetDB(d *db.DB) {
|
||||||
c.dbMu.Lock()
|
c.dbMu.Lock()
|
||||||
c.db = d
|
c.db = d
|
||||||
@@ -65,6 +67,10 @@ func (c *Copier) Start(ctx context.Context, opts Options) (string, error) {
|
|||||||
return "", errors.New("no disk database available")
|
return "", errors.New("no disk database available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.DestFolder == "" {
|
||||||
|
opts.DestFolder = "media"
|
||||||
|
}
|
||||||
|
|
||||||
t := c.tasks.Create("copy")
|
t := c.tasks.Create("copy")
|
||||||
copyCtx, cancel := context.WithCancel(ctx)
|
copyCtx, cancel := context.WithCancel(ctx)
|
||||||
c.cancel = cancel
|
c.cancel = cancel
|
||||||
@@ -98,9 +104,11 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
|||||||
|
|
||||||
setStatus(task.StatusRunning, "Подготовка…", 0)
|
setStatus(task.StatusRunning, "Подготовка…", 0)
|
||||||
|
|
||||||
|
destRoot := filepath.Join(opts.MountPath, opts.DestFolder)
|
||||||
|
|
||||||
if opts.OverwriteMode == config.OverwriteDelete {
|
if opts.OverwriteMode == config.OverwriteDelete {
|
||||||
setStatus(task.StatusRunning, "Удаление данных с диска…", 0)
|
setStatus(task.StatusRunning, "Удаление данных с диска…", 0)
|
||||||
if err := deleteOurData(opts.MountPath); err != nil {
|
if err := os.RemoveAll(destRoot); err != nil {
|
||||||
fail(err)
|
fail(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -128,6 +136,9 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// случайный порядок — выбираем что копировать до начала копирования
|
||||||
|
rand.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] })
|
||||||
|
|
||||||
_, free, err := disk.DiskUsage(opts.MountPath)
|
_, free, err := disk.DiskUsage(opts.MountPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(err)
|
fail(err)
|
||||||
@@ -140,14 +151,25 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// суммарный объём для прогресса (всех файлов в списке)
|
||||||
|
var totalBytes int64
|
||||||
|
for _, f := range files {
|
||||||
|
totalBytes += f.size
|
||||||
|
}
|
||||||
|
|
||||||
total := len(files)
|
total := len(files)
|
||||||
copied := 0
|
copied := 0
|
||||||
|
var doneBytes int64
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
c.tasks.Update(taskID, func(t *task.Task) {
|
c.tasks.Update(taskID, func(t *task.Task) {
|
||||||
t.Status = task.StatusCanceled
|
t.Status = task.StatusCanceled
|
||||||
t.Message = "Отменено"
|
t.Message = "Отменено"
|
||||||
|
t.SpeedBPS = 0
|
||||||
|
t.ETASec = 0
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
@@ -157,16 +179,35 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf("Копирование %s (%d/%d)", filepath.Base(f.srcAbs), i+1, total)
|
elapsed := time.Since(startTime).Seconds()
|
||||||
prog := int(float64(i+1) / float64(total) * 100)
|
var speedBPS, etaSec int64
|
||||||
setStatus(task.StatusRunning, msg, prog)
|
if elapsed > 0 && doneBytes > 0 {
|
||||||
|
speedBPS = int64(float64(doneBytes) / elapsed)
|
||||||
|
remaining := totalBytes - doneBytes
|
||||||
|
if speedBPS > 0 {
|
||||||
|
etaSec = remaining / speedBPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dstAbs := filepath.Join(opts.MountPath, f.relPath)
|
prog := int(float64(doneBytes) / float64(totalBytes) * 100)
|
||||||
if err := copyFile(ctx, f.srcAbs, dstAbs); err != nil {
|
msg := fmt.Sprintf("Копирование %s (%d/%d)", filepath.Base(f.srcAbs), i+1, total)
|
||||||
|
|
||||||
|
c.tasks.Update(taskID, func(t *task.Task) {
|
||||||
|
t.Status = task.StatusRunning
|
||||||
|
t.Message = msg
|
||||||
|
t.Progress = prog
|
||||||
|
t.SpeedBPS = speedBPS
|
||||||
|
t.ETASec = int(etaSec)
|
||||||
|
})
|
||||||
|
|
||||||
|
dstAbs := filepath.Join(destRoot, f.relPath)
|
||||||
|
if err := rsyncFile(ctx, f.srcAbs, dstAbs); err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
c.tasks.Update(taskID, func(t *task.Task) {
|
c.tasks.Update(taskID, func(t *task.Task) {
|
||||||
t.Status = task.StatusCanceled
|
t.Status = task.StatusCanceled
|
||||||
t.Message = "Отменено"
|
t.Message = "Отменено"
|
||||||
|
t.SpeedBPS = 0
|
||||||
|
t.ETASec = 0
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -174,6 +215,7 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
|||||||
}
|
}
|
||||||
|
|
||||||
available -= f.size
|
available -= f.size
|
||||||
|
doneBytes += f.size
|
||||||
copied++
|
copied++
|
||||||
_ = database.RecordCopy(db.CopyRecord{
|
_ = database.RecordCopy(db.CopyRecord{
|
||||||
DiskID: opts.DiskID,
|
DiskID: opts.DiskID,
|
||||||
@@ -187,7 +229,7 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
|||||||
|
|
||||||
type fileEntry struct {
|
type fileEntry struct {
|
||||||
srcAbs string
|
srcAbs string
|
||||||
relPath string
|
relPath string // relative to /media
|
||||||
size int64
|
size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,64 +259,29 @@ func buildFileList(mediaPath string, sources []string, skip map[string]struct{})
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteOurData(mountPath string) error {
|
// rsyncFile copies src to dst using rsync with resume support.
|
||||||
entries, err := os.ReadDir(mountPath)
|
// --partial keeps partial files on interruption.
|
||||||
if err != nil {
|
// --append-verify resumes partial transfers and verifies checksums.
|
||||||
return err
|
func rsyncFile(ctx context.Context, src, dst string) error {
|
||||||
}
|
|
||||||
for _, e := range entries {
|
|
||||||
if e.Name() == ".jukebox" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(filepath.Join(mountPath, e.Name())); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFile(ctx context.Context, src, dst string) error {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
in, err := os.Open(src)
|
cmd := exec.CommandContext(ctx, "rsync",
|
||||||
|
"--partial",
|
||||||
|
"--append-verify",
|
||||||
|
"--times",
|
||||||
|
"--no-perms",
|
||||||
|
"--no-owner",
|
||||||
|
"--no-group",
|
||||||
|
"--chmod=ugo=rwx",
|
||||||
|
src, dst,
|
||||||
|
)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if ctx.Err() != nil {
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
tmp := dst + ".juketmp"
|
|
||||||
out, err := os.Create(tmp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 512*1024)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
out.Close()
|
|
||||||
os.Remove(tmp)
|
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
|
||||||
}
|
|
||||||
n, readErr := in.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
if _, werr := out.Write(buf[:n]); werr != nil {
|
|
||||||
out.Close()
|
|
||||||
os.Remove(tmp)
|
|
||||||
return werr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if errors.Is(readErr, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if readErr != nil {
|
|
||||||
out.Close()
|
|
||||||
os.Remove(tmp)
|
|
||||||
return readErr
|
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("rsync: %w: %s", err, out)
|
||||||
}
|
}
|
||||||
out.Close()
|
return nil
|
||||||
return os.Rename(tmp, dst)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type Task struct {
|
|||||||
Status Status `json:"status"`
|
Status Status `json:"status"`
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
SpeedBPS int64 `json:"speed_bps"`
|
||||||
|
ETASec int `json:"eta_sec"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
@@ -30,7 +30,10 @@
|
|||||||
<div class="progress-bar-bg">
|
<div class="progress-bar-bg">
|
||||||
<div class="progress-bar-fill" id="progressFill" style="width:0%"></div>
|
<div class="progress-bar-fill" id="progressFill" style="width:0%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-label" id="progressMsg">Подготовка…</div>
|
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-top:6px">
|
||||||
|
<div class="progress-label" id="progressMsg">Подготовка…</div>
|
||||||
|
<div class="progress-label" id="progressMeta" style="text-align:right"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -83,6 +86,21 @@ async function refreshDisk() {
|
|||||||
function startTaskPoll(id) { stopTaskPoll(); pollInterval = setInterval(() => pollTask(id), 1500); }
|
function startTaskPoll(id) { stopTaskPoll(); pollInterval = setInterval(() => pollTask(id), 1500); }
|
||||||
function stopTaskPoll() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } }
|
function stopTaskPoll() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } }
|
||||||
|
|
||||||
|
function fmtSpeed(bps) {
|
||||||
|
if (!bps) return '';
|
||||||
|
if (bps >= 1e9) return (bps/1e9).toFixed(1) + ' ГБ/с';
|
||||||
|
if (bps >= 1e6) return (bps/1e6).toFixed(1) + ' МБ/с';
|
||||||
|
if (bps >= 1e3) return (bps/1e3).toFixed(0) + ' КБ/с';
|
||||||
|
return bps + ' Б/с';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtETA(sec) {
|
||||||
|
if (!sec || sec <= 0) return '';
|
||||||
|
if (sec >= 3600) return Math.floor(sec/3600) + ' ч ' + Math.floor((sec%3600)/60) + ' мин';
|
||||||
|
if (sec >= 60) return Math.floor(sec/60) + ' мин';
|
||||||
|
return sec + ' с';
|
||||||
|
}
|
||||||
|
|
||||||
async function pollTask(id) {
|
async function pollTask(id) {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/tasks/' + id);
|
const r = await fetch('/api/tasks/' + id);
|
||||||
@@ -90,12 +108,18 @@ async function pollTask(id) {
|
|||||||
const t = await r.json();
|
const t = await r.json();
|
||||||
document.getElementById('progressFill').style.width = t.progress + '%';
|
document.getElementById('progressFill').style.width = t.progress + '%';
|
||||||
document.getElementById('progressMsg').textContent = t.message || '…';
|
document.getElementById('progressMsg').textContent = t.message || '…';
|
||||||
|
|
||||||
|
const speed = fmtSpeed(t.speed_bps);
|
||||||
|
const eta = fmtETA(t.eta_sec);
|
||||||
|
const meta = [speed, eta ? 'ETA: ' + eta : ''].filter(Boolean).join(' · ');
|
||||||
|
document.getElementById('progressMeta').textContent = meta;
|
||||||
if (['success','failed','canceled'].includes(t.status)) {
|
if (['success','failed','canceled'].includes(t.status)) {
|
||||||
stopTaskPoll(); activeTaskId = null;
|
stopTaskPoll(); activeTaskId = null;
|
||||||
document.getElementById('btnStart').disabled = false;
|
document.getElementById('btnStart').disabled = false;
|
||||||
document.getElementById('btnStart').classList.remove('hidden');
|
document.getElementById('btnStart').classList.remove('hidden');
|
||||||
document.getElementById('btnCancel').classList.add('hidden');
|
document.getElementById('btnCancel').classList.add('hidden');
|
||||||
document.getElementById('progressPanel').classList.add('hidden');
|
document.getElementById('progressPanel').classList.add('hidden');
|
||||||
|
document.getElementById('progressMeta').textContent = '';
|
||||||
if (t.status === 'success') toast(t.message || 'Готово', 'ok');
|
if (t.status === 'success') toast(t.message || 'Готово', 'ok');
|
||||||
if (t.status === 'failed') toast('Ошибка: ' + t.error, 'error');
|
if (t.status === 'failed') toast('Ошибка: ' + t.error, 'error');
|
||||||
if (t.status === 'canceled') toast('Копирование отменено', 'error');
|
if (t.status === 'canceled') toast('Копирование отменено', 'error');
|
||||||
|
|||||||
@@ -30,13 +30,19 @@
|
|||||||
<span class="form-hint">«Только новые» — пропускает файлы, уже скопированные на данный диск, даже если они были удалены с него (считаются просмотренными).</span>
|
<span class="form-hint">«Только новые» — пропускает файлы, уже скопированные на данный диск, даже если они были удалены с него (считаются просмотренными).</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="destFolder">Папка назначения на диске</label>
|
||||||
|
<input class="form-input" type="text" id="destFolder" placeholder="media" style="width:200px">
|
||||||
|
<span class="form-hint">Подпапка на диске куда копировать файлы. Структура источника воспроизводится внутри неё. По умолчанию: <code>media</code>.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="overwriteMode">Режим записи</label>
|
<label class="form-label" for="overwriteMode">Режим записи</label>
|
||||||
<select class="form-select" id="overwriteMode" style="width:auto;max-width:420px">
|
<select class="form-select" id="overwriteMode" style="width:auto;max-width:420px">
|
||||||
<option value="skip">Пропустить существующие файлы</option>
|
<option value="skip">Пропустить существующие файлы</option>
|
||||||
<option value="delete">Удалить наши данные с диска и перезаписать заново</option>
|
<option value="delete">Удалить папку назначения и перезаписать заново</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="form-hint">«Удалить и перезаписать» — удаляет с диска всё кроме папки .jukebox, затем копирует заново.</span>
|
<span class="form-hint">«Удалить и перезаписать» — удаляет папку назначения на диске, затем копирует заново.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +104,7 @@ async function loadSettings() {
|
|||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
const cfg = await r.json();
|
const cfg = await r.json();
|
||||||
document.getElementById('reserveGB').value = cfg.reserve_free_gb ?? 2;
|
document.getElementById('reserveGB').value = cfg.reserve_free_gb ?? 2;
|
||||||
|
document.getElementById('destFolder').value = cfg.dest_folder || 'media';
|
||||||
document.getElementById('fileSelectMode').value = cfg.file_select_mode || 'new';
|
document.getElementById('fileSelectMode').value = cfg.file_select_mode || 'new';
|
||||||
document.getElementById('overwriteMode').value = cfg.overwrite_mode || 'skip';
|
document.getElementById('overwriteMode').value = cfg.overwrite_mode || 'skip';
|
||||||
document.getElementById('autoCopy').checked = !!cfg.auto_copy;
|
document.getElementById('autoCopy').checked = !!cfg.auto_copy;
|
||||||
@@ -116,6 +123,7 @@ async function saveSettings(e) {
|
|||||||
});
|
});
|
||||||
const body = {
|
const body = {
|
||||||
reserve_free_gb: parseFloat(document.getElementById('reserveGB').value) || 2,
|
reserve_free_gb: parseFloat(document.getElementById('reserveGB').value) || 2,
|
||||||
|
dest_folder: document.getElementById('destFolder').value.trim() || 'media',
|
||||||
file_select_mode: document.getElementById('fileSelectMode').value,
|
file_select_mode: document.getElementById('fileSelectMode').value,
|
||||||
overwrite_mode: document.getElementById('overwriteMode').value,
|
overwrite_mode: document.getElementById('overwriteMode').value,
|
||||||
auto_copy: document.getElementById('autoCopy').checked,
|
auto_copy: document.getElementById('autoCopy').checked,
|
||||||
|
|||||||
Reference in New Issue
Block a user