From 70438b193bfe6368a01c50b04bbdfe9400ca36b0 Mon Sep 17 00:00:00 2001 From: Brandon Doornbos <41441504+brandon-doornbos@users.noreply.github.com> Date: Sat, 23 May 2026 22:46:36 +0200 Subject: [PATCH] Prototype WOPI integration --- copyparty/__main__.py | 7 ++++ copyparty/cfg.py | 1 + copyparty/httpcli.py | 76 ++++++++++++++++++++++++++++++++++++++++ copyparty/web/browser.js | 5 ++- 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 51ceefab..604c0210 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1568,6 +1568,12 @@ def add_opds(ap): ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)") +def add_wopi(ap): + ap2 = ap.add_argument_group("WOPI options") + ap2.add_argument("--wopi", action="store_true", help="enable WOPI -- allows for integrating with office suites (volflag=wopi)") + ap2.add_argument("--wopi-path", action="store_true", default="wopi", help="where to expose WOPI (needs to start with 'wopi'); defaults to /wopi/ (volflag=wopi-path)") + + def add_handlers(ap): ap2 = ap.add_argument_group("handlers (see --help-handlers)") ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file") @@ -2045,6 +2051,7 @@ def run_argparse( add_tftp(ap) add_smb(ap) add_opds(ap) + add_wopi(ap) add_safety(ap) add_salt(ap, fk_salt, dk_salt, ah_salt) add_optouts(ap) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index d4c163bb..a45fa780 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -240,6 +240,7 @@ flagcats = { "gz": "allows server-side gzip compression of uploads with ?gz", "xz": "allows server-side lzma compression of uploads with ?xz", "pk": "forces server-side compression, optional arg: xz,9", + "wopi": "enable WOPI support for integrating with online office suites", }, "upload rules": { "apnd_who=dw": "who can append? (aw/dw/w/no)", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index e4cd6a36..66bcd48d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1413,6 +1413,8 @@ class HttpCli(object): self.tx_404() return False + elif self.vpath.startswith("wopi"): + return self.tx_wopi() if "cf_challenge" in self.uparam: self.reply(self.j2s("cf").encode("utf-8", "replace")) @@ -1526,6 +1528,47 @@ class HttpCli(object): return self.tx_browser() + def tx_wopi(self) -> bool: + path = self.vpath.split('/') + full_path = "/mnt/tmp/" + path[2] + + if self.do_log: + self.log("WOPI GET %s" % (path[2])) + + if path[1] == "files": + if len(path) > 3 and path[3] == "contents": + return self.tx_file("oh_f", full_path) + else: + file_info = { + "BaseFileName": path[2], + "OwnerId": self.uname, + "Size": os.path.getsize(full_path), + "UserId": self.uname, + "UserFriendlyName": self.uname, + "UserCanWrite": True, + # "UserCanNotWriteRelative": False, + # "PostMessageOrigin": , + # "HidePrintOption": , + # "DisablePrint": , + # "HideSaveOption": , + # "HideExportOption": , + # "HideRepairOption": , + # "DisableExport": , + # "DisableCopy": , + "EnableOwnerTermination": True, + "LastModifiedTime": + time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(os.path.getmtime(full_path))), + # "IsUserLocked": , + # "IsUserRestricted": , + # "SupportsRename": True, + # "UserCanRename": True, + } + ret = json.dumps(file_info).encode("utf-8", "replace") + self.reply(ret, 200, "application/json; charset=utf-8") + return True + + return self.tx_404(True) + def tx_rss(self) -> bool: if self.do_log: self.log("RSS %s @%s" % (self.req, self.uname)) @@ -3149,6 +3192,9 @@ class HttpCli(object): except: raise Pebkac(400, "you must supply a content-length for binary POST") + if self.vpath.startswith("wopi"): + return self.rx_wopi() + try: chashes = self.headers["x-up2k-hash"].split(",") wark = self.headers["x-up2k-wark"] @@ -3360,6 +3406,36 @@ class HttpCli(object): self.reply(b"thank") return True + def rx_wopi(self) -> bool: + path = self.vpath.split('/') + full_path = "/mnt/tmp/" + path[2] + + if self.do_log: + self.log("WOPI POST %s" % (path[2])) + + if path[1] == "files": + if len(path) > 3 and path[3] == "contents": + reader, _ = self.get_body_reader() + buf = b"" + for rbuf in reader: + buf += rbuf + if not rbuf or len(buf) >= 32768: + break + + if buf: + with open(full_path, "wb") as file: + file.write(buf) + + mod_time = { + "LastModifiedTime": + time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(os.path.getmtime(full_path))) + } + ret = json.dumps(mod_time).encode("utf-8", "replace") + self.reply(ret, 200, "application/json; charset=utf-8") + return True + + return self.tx_404(True) + def handle_chpw(self) -> bool: assert self.parser # !rm if self.args.usernames: diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 6622c5c1..4c214056 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -7665,7 +7665,10 @@ var treectl = (function () { tn.href = addq(tn.href, 'v'); } - if (tn.lead == '-') + if (["ods", "odt", "odp"].includes(tn.ext)) { + tn.lead = '📄'; + } else if (tn.lead == '-') tn.lead = '-txt-';