diff --git a/README.md b/README.md index 47a89d3b..170d2f56 100644 --- a/README.md +++ b/README.md @@ -2684,6 +2684,10 @@ interact with copyparty using non-browser clients * `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}` `chunk /dev/tcp/127.0.0.1/3923` diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3b07e44d..bd7fc51d 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1232,6 +1232,7 @@ def add_upload(ap): ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h") ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)") + ap2.add_argument("--apnd-who", metavar="NUM", type=u, default="dw", help="who can append to existing files? [\033[32mno\033[0m]=nobody, [\033[32maw\033[0m]=admin+write, [\033[32mdw\033[0m]=delete+write, [\033[32mw\033[0m]=write (volflag=apnd_who)") ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)") ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600") diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 92b19153..d33287e9 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -94,6 +94,7 @@ def vf_vmap() -> dict[str, str]: "th_x3": "th3x", } for k in ( + "apnd_who", "bup_ck", "cachectl", "casechk", @@ -232,6 +233,7 @@ flagcats = { "pk": "forces server-side compression, optional arg: xz,9", }, "upload rules": { + "apnd_who=dw": "who can append? (aw/dw/w/no)", "maxn=250,600": "max 250 uploads over 15min", "maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)", "vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 0c17fa8b..9fa05b0a 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -2478,7 +2478,12 @@ class HttpCli(object): self.vpath = vjoin(self.vpath, fn) params["fdir"] = fdir - if is_put and not (self.args.no_dav or self.args.nw) and bos.path.exists(path): + if ( + is_put + and not (self.args.no_dav or self.args.nw) + and "append" not in self.uparam + and bos.path.exists(path) + ): # allow overwrite if... # * volflag 'daw' is set, or client is definitely webdav # * and account has delete-access @@ -2526,7 +2531,22 @@ class HttpCli(object): else: raise Pebkac(500, "unknown hash alg") - f, fn = ren_open(fn, *open_a, **params) + if "apnd" in self.uparam and not self.args.nw and bos.path.exists(path): + zs = vfs.flags["apnd_who"] + if ( + zs == "w" + or (zs == "aw" and self.can_admin) + or (zs == "dw" and self.can_delete) + ): + pass + else: + raise Pebkac(400, "you do not have permission to append") + zs = os.path.join(params["fdir"], fn) + self.log("upload will append to [%s]" % (zs,)) + f = open(zs, "ab") + else: + f, fn = ren_open(fn, *open_a, **params) + try: path = os.path.join(fdir, fn) post_sz, sha_hex, sha_b64 = copier(reader, f, hasher, 0, self.args.s_wr_slp) diff --git a/docs/devnotes.md b/docs/devnotes.md index 37e1614e..bf8290f3 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -226,11 +226,13 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo` | PUT | `?ck=md5` | (binary data) | return md5 instead of sha512 | | PUT | `?gz` | (binary data) | compress with gzip and write into file at URL | | PUT | `?xz` | (binary data) | compress with xz and write into file at URL | +| PUT | `?apnd` | (binary data) | append to existing file | | mPOST | | `f=FILE` | upload `FILE` into the folder at URL | | mPOST | `?j` | `f=FILE` | ...and reply with json | | mPOST | `?ck` | `f=FILE` | ...and disable checksum gen (faster) | | mPOST | `?ck=md5` | `f=FILE` | ...and return md5 instead of sha512 | | mPOST | `?replace` | `f=FILE` | ...and overwrite existing files | +| mPOST | `?apnd` | `f=FILE` | ...and append to existing files | | mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) | | mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL | | POST | `?delete` | | delete URL recursively | diff --git a/tests/util.py b/tests/util.py index 184caae4..85209f3b 100644 --- a/tests/util.py +++ b/tests/util.py @@ -167,7 +167,7 @@ class Cfg(Namespace): ex = "ah_alg bname chdir chmod_f chpw_db doctitle df epilogues exit favico ipa ipar html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) - ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl rss_fmt_d rss_fmt_t spinner" + ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl rss_fmt_d rss_fmt_t spinner" ka.update(**{k: "no" for k in ex.split()}) ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm"