up2k: tristate option for overwriting files; closes #139

adds a third possible value for the `replace` property in handshakes:

* absent or False: never overwrite an existing file on the server,
   and instead generate a new filename to avoid collision

* True: always overwrite existing files on the server

* "mt": only overwrite if client's last-modified is more recent
   (this is the new option)

the new UI button toggles between all three options,
defaulting to never-overwrite
This commit is contained in:
ed 2025-02-19 21:58:56 +00:00
parent 6858cb066f
commit e9f78ea70c
9 changed files with 58 additions and 16 deletions

View file

@ -780,8 +780,11 @@ the up2k UI is the epitome of polished intuitive experiences:
* "parallel uploads" specifies how many chunks to upload at the same time * "parallel uploads" specifies how many chunks to upload at the same time
* `[🏃]` analysis of other files should continue while one is uploading * `[🏃]` analysis of other files should continue while one is uploading
* `[🥔]` shows a simpler UI for faster uploads from slow devices * `[🥔]` shows a simpler UI for faster uploads from slow devices
* `[🛡️]` decides when to overwrite existing files on the server
* `🛡️` = never (generate a new filename instead)
* `🕒` = overwrite if the server-file is older
* `♻️` = always overwrite if the files are different
* `[🎲]` generate random filenames during upload * `[🎲]` generate random filenames during upload
* `[📅]` preserve last-modified timestamps; server times will match yours
* `[🔎]` switch between upload and [file-search](#file-search) mode * `[🔎]` switch between upload and [file-search](#file-search) mode
* ignore `[🔎]` if you add files by dragging them into the browser * ignore `[🔎]` if you add files by dragging them into the browser

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
S_VERSION = "2.9" S_VERSION = "2.10"
S_BUILD_DT = "2025-01-27" S_BUILD_DT = "2025-02-19"
""" """
u2c.py: upload to copyparty u2c.py: upload to copyparty
@ -807,7 +807,9 @@ def handshake(ar, file, search):
else: else:
if ar.touch: if ar.touch:
req["umod"] = True req["umod"] = True
if ar.ow: if ar.owo:
req["replace"] = "mt"
elif ar.ow:
req["replace"] = True req["replace"] = True
file.recheck = False file.recheck = False
@ -1538,6 +1540,7 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible") ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)") ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming") ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
ap.add_argument("--owo", action="store_true", help="overwrite existing files if server-file is older")
ap.add_argument("--spd", action="store_true", help="print speeds for each file") ap.add_argument("--spd", action="store_true", help="print speeds for each file")
ap.add_argument("--version", action="store_true", help="show version and exit") ap.add_argument("--version", action="store_true", help="show version and exit")

View file

@ -1039,6 +1039,7 @@ def add_upload(ap):
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck") ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good for low-latency (same-country) connections, 4-8 for android clients, 16 for cross-atlantic (max=64)") ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good for low-latency (same-country) connections, 4-8 for android clients, 16 for cross-atlantic (max=64)")
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]") ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine") ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory") ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")

View file

@ -1956,11 +1956,7 @@ class AuthSrv(object):
if vf not in vol.flags: if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga) vol.flags[vf] = getattr(self.args, ga)
for k in ("nrand",): zs = "forget_ip nrand u2abort u2ow ups_who zip_who"
if k not in vol.flags:
vol.flags[k] = getattr(self.args, k)
zs = "forget_ip nrand u2abort ups_who zip_who"
for k in zs.split(): for k in zs.split():
if k in vol.flags: if k in vol.flags:
vol.flags[k] = int(vol.flags[k]) vol.flags[k] = int(vol.flags[k])
@ -2436,6 +2432,7 @@ class AuthSrv(object):
"u2j": self.args.u2j, "u2j": self.args.u2j,
"u2sz": self.args.u2sz, "u2sz": self.args.u2sz,
"u2ts": vf["u2ts"], "u2ts": vf["u2ts"],
"u2ow": vf["u2ow"],
"frand": bool(vf.get("rand")), "frand": bool(vf.get("rand")),
"lifetime": vn.js_ls["lifetime"], "lifetime": vn.js_ls["lifetime"],
"u2sort": self.args.u2sort, "u2sort": self.args.u2sort,

View file

@ -82,6 +82,7 @@ def vf_vmap() -> dict[str, str]:
"lg_sba", "lg_sba",
"md_sba", "md_sba",
"nrand", "nrand",
"u2ow",
"og_desc", "og_desc",
"og_site", "og_site",
"og_th", "og_th",
@ -170,6 +171,7 @@ flagcats = {
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)", "medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
"rand": "force randomized filenames, 9 chars long by default", "rand": "force randomized filenames, 9 chars long by default",
"nrand=N": "randomized filenames are N chars long", "nrand=N": "randomized filenames are N chars long",
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time", "u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
"u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk", "u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB", "sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",

View file

@ -3373,7 +3373,17 @@ class Up2k(object):
return fname return fname
fp = djoin(fdir, fname) fp = djoin(fdir, fname)
if job.get("replace") and bos.path.exists(fp):
ow = job.get("replace") and bos.path.exists(fp)
if ow and "mt" in str(job["replace"]).lower():
mts = bos.stat(fp).st_mtime
mtc = job["lmod"]
if mtc < mts:
t = "will not overwrite; server %d sec newer than client; %d > %d %r"
self.log(t % (mts - mtc, mts, mtc, fp))
ow = False
if ow:
self.log("replacing existing file at %r" % (fp,)) self.log("replacing existing file at %r" % (fp,))
cur = None cur = None
ptop = job["ptop"] ptop = job["ptop"]

View file

@ -151,7 +151,8 @@ var Ls = {
"ul_par": "parallel uploads:", "ul_par": "parallel uploads:",
"ut_rand": "randomize filenames", "ut_rand": "randomize filenames",
"ut_u2ts": "copy the last-modified timestamp$Nfrom your filesystem to the server", "ut_u2ts": "copy the last-modified timestamp$Nfrom your filesystem to the server\">📅",
"ut_ow": "overwrite existing files on the server?$N🛡: never (will generate a new filename instead)$N🕒: overwrite if server-file is older than yours$N♻: always overwrite if the files are different",
"ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck", "ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck",
"ut_ask": 'ask for confirmation before upload starts">💭', "ut_ask": 'ask for confirmation before upload starts">💭',
"ut_pot": "improve upload speed on slow devices$Nby making the UI less complex", "ut_pot": "improve upload speed on slow devices$Nby making the UI less complex",
@ -751,7 +752,8 @@ var Ls = {
"ul_par": "samtidige handl.:", "ul_par": "samtidige handl.:",
"ut_rand": "finn opp nye tilfeldige filnavn", "ut_rand": "finn opp nye tilfeldige filnavn",
"ut_u2ts": "gi filen på serveren samme$Ntidsstempel som lokalt hos deg", "ut_u2ts": "gi filen på serveren samme$Ntidsstempel som lokalt hos deg\">📅",
"ut_ow": "overskrive eksisterende filer på serveren?$N🛡: aldri (finner på et nytt filnavn istedenfor)$N🕒: overskriv hvis serverens fil er eldre$N♻: alltid, gitt at innholdet er forskjellig",
"ut_mt": "fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk", "ut_mt": "fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk",
"ut_ask": 'bekreft filutvalg før opplastning starter">💭', "ut_ask": 'bekreft filutvalg før opplastning starter">💭',
"ut_pot": "forbedre ytelsen på trege enheter ved å$Nforenkle brukergrensesnittet", "ut_pot": "forbedre ytelsen på trege enheter ved å$Nforenkle brukergrensesnittet",
@ -1351,7 +1353,8 @@ var Ls = {
"ul_par": "并行上传:", "ul_par": "并行上传:",
"ut_rand": "随机化文件名", "ut_rand": "随机化文件名",
"ut_u2ts": "将最后修改的时间戳$N从你的文件系统复制到服务器", "ut_u2ts": "将最后修改的时间戳$N从你的文件系统复制到服务器\">📅",
"ut_ow": "覆盖服务器上的现有文件?$N🛡: 从不(会生成一个新文件名)$N🕒: 服务器文件较旧则覆盖$N♻: 总是覆盖,如果文件内容不同", //m
"ut_mt": "在上传时继续哈希其他文件$N$N如果你的 CPU 或硬盘是瓶颈,可能需要禁用", "ut_mt": "在上传时继续哈希其他文件$N$N如果你的 CPU 或硬盘是瓶颈,可能需要禁用",
"ut_ask": '上传开始前询问确认">💭', "ut_ask": '上传开始前询问确认">💭',
"ut_pot": "通过简化 UI 来$N提高慢设备上的上传速度", "ut_pot": "通过简化 UI 来$N提高慢设备上的上传速度",
@ -1918,8 +1921,8 @@ ebi('op_up2k').innerHTML = (
' <label for="u2rand" tt="' + L.ut_rand + '">🎲</label>\n' + ' <label for="u2rand" tt="' + L.ut_rand + '">🎲</label>\n' +
' </td>\n' + ' </td>\n' +
' <td class="c" rowspan="2">\n' + ' <td class="c" rowspan="2">\n' +
' <input type="checkbox" id="u2ts" />\n' + ' <input type="checkbox" id="u2ow" />\n' +
' <label for="u2ts" tt="' + L.ut_u2ts + '">📅</a>\n' + ' <label for="u2ow" tt="' + L.ut_ow + '">?</a>\n' +
' </td>\n' + ' </td>\n' +
' <td class="c" data-perm="read" data-dep="idx" rowspan="2">\n' + ' <td class="c" data-perm="read" data-dep="idx" rowspan="2">\n' +
' <input type="checkbox" id="fsearch" />\n' + ' <input type="checkbox" id="fsearch" />\n' +
@ -2037,6 +2040,7 @@ ebi('op_cfg').innerHTML = (
' <h3>' + L.cl_uopts + '</h3>\n' + ' <h3>' + L.cl_uopts + '</h3>\n' +
' <div>\n' + ' <div>\n' +
' <a id="ask_up" class="tgl btn" href="#" tt="' + L.ut_ask + '</a>\n' + ' <a id="ask_up" class="tgl btn" href="#" tt="' + L.ut_ask + '</a>\n' +
' <a id="u2ts" class="tgl btn" href="#" tt="' + L.ut_u2ts + '</a>\n' +
' <a id="umod" class="tgl btn" href="#" tt="' + L.cut_umod + '</a>\n' + ' <a id="umod" class="tgl btn" href="#" tt="' + L.cut_umod + '</a>\n' +
' <a id="hashw" class="tgl btn" href="#" tt="' + L.cut_mt + '</a>\n' + ' <a id="hashw" class="tgl btn" href="#" tt="' + L.cut_mt + '</a>\n' +
' <a id="u2turbo" class="tgl btn ttb" href="#" tt="' + L.cut_turbo + '</a>\n' + ' <a id="u2turbo" class="tgl btn ttb" href="#" tt="' + L.cut_turbo + '</a>\n' +

View file

@ -885,6 +885,21 @@ function up2k_init(subtle) {
bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag); bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag);
bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx); bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx);
uc.ow = parseInt(sread('u2ow', ['0', '1', '2']) || u2ow);
uc.owt = ['🛡️', '🕒', '♻️'];
function set_ow() {
QS('label[for="u2ow"]').innerHTML = uc.owt[uc.ow];
ebi('u2ow').checked = true; //cosmetic
}
ebi('u2ow').onclick = function (e) {
ev(e);
if (++uc.ow > 2)
uc.ow = 0;
swrite('u2ow', uc.ow);
set_ow();
};
set_ow();
var st = { var st = {
"files": [], "files": [],
"nfile": { "nfile": {
@ -2634,6 +2649,13 @@ function up2k_init(subtle) {
else if (t.umod) else if (t.umod)
req.umod = true; req.umod = true;
if (!t.srch) {
if (uc.ow == 1)
req.replace = 'mt';
if (uc.ow == 2)
req.replace = true;
}
xhr.open('POST', t.purl, true); xhr.open('POST', t.purl, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 : xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 :

View file

@ -144,7 +144,7 @@ class Cfg(Namespace):
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody th_convt ups_who zip_who" ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody th_convt ups_who zip_who"
ka.update(**{k: 9 for k in ex.split()}) ka.update(**{k: 9 for k in ex.split()})
ex = "db_act forget_ip k304 loris no304 re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo" ex = "db_act forget_ip k304 loris no304 re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow"
ka.update(**{k: 0 for k in ex.split()}) ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src R RS SR" ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src R RS SR"