From f55d8341f1de89bdea025a13f33e179090f46a4d Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 14 Oct 2025 22:51:48 +0000 Subject: [PATCH] new flag nodupem: reject dupes on move; closes #742 --- copyparty/__main__.py | 3 ++- copyparty/cfg.py | 2 ++ copyparty/up2k.py | 6 ++++++ copyparty/util.py | 2 ++ tests/util.py | 2 +- 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 50d3b567..bc2bdfe0 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1241,6 +1241,7 @@ def add_upload(ap): ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)") ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)") ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)") + ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)") ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)") ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually") ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash") @@ -2079,7 +2080,7 @@ def main(argv: Optional[list[str]] = None) -> None: # propagate implications for k1, k2 in IMPLICATIONS: - if getattr(al, k1): + if getattr(al, k1, None): setattr(al, k2, True) # propagate unplications diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 93c146af..0c50bcb0 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -19,6 +19,7 @@ def vf_bmap() -> dict[str, str]: "no_clone": "noclone", "no_dirsz": "nodirsz", "no_dupe": "nodupe", + "no_dupe_m": "nodupem", "no_forget": "noforget", "no_pipe": "nopipe", "no_robots": "norobots", @@ -189,6 +190,7 @@ flagcats = { "safededup": "verify on-disk data before using it for dedup", "noclone": "take dupe data from clients, even if available on HDD", "nodupe": "rejects existing files (instead of linking/cloning them)", + "nodupem": "rejects existing files during moves as well", "chmod_d=755": "unix-permission for new dirs/folders", "chmod_f=644": "unix-permission for new files", "uid=573": "change owner of new files/folders to unix-user 573", diff --git a/copyparty/up2k.py b/copyparty/up2k.py index a85e379d..4f33bb0c 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -4708,6 +4708,12 @@ class Up2k(object): if w: assert c1 # !rm if c2 and c2 != c1: + if "nodupem" in dvn.flags: + q = "select w from up where substr(w,1,16) = ?" + for (w2,) in c2.execute(q, (w[:16],)): + if w == w2: + t = "file exists in target volume, and dupes are forbidden in config" + raise Pebkac(400, t) self._copy_tags(c1, c2, w) xlink = bool(svn.flags.get("xlink")) diff --git a/copyparty/util.py b/copyparty/util.py index 1837233b..3c9ed2ab 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -358,6 +358,8 @@ IMPLICATIONS = [ ["hardlink_only", "hardlink"], ["hardlink", "dedup"], ["tftpvv", "tftpv"], + ["nodupem", "nodupe"], + ["no_dupe_m", "no_dupe"], ["smbw", "smb"], ["smb1", "smb"], ["smbvvv", "smbvv"], diff --git a/tests/util.py b/tests/util.py index 306bda10..d5fb34af 100644 --- a/tests/util.py +++ b/tests/util.py @@ -143,7 +143,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss smb srch_dbg srch_excl srch_icase stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" + ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss smb srch_dbg srch_excl srch_icase stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ka.update(**{k: False for k in ex.split()}) ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"