From 9d223d6ca7c49f57dc8b31a511f4f3ee975f2653 Mon Sep 17 00:00:00 2001 From: Toast <39011842+toast003@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:08:57 +0100 Subject: [PATCH 01/54] nixos: use mkPackageOption instead of mkOption (#1193) This gets rid of a warning when trying to build a system with documentation.nixos.includeAllModules enabled --- contrib/nixos/modules/copyparty.nix | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contrib/nixos/modules/copyparty.nix b/contrib/nixos/modules/copyparty.nix index 6bb51b68..612672ee 100644 --- a/contrib/nixos/modules/copyparty.nix +++ b/contrib/nixos/modules/copyparty.nix @@ -69,11 +69,8 @@ in options.services.copyparty = { enable = mkEnableOption "web-based file manager"; - package = mkOption { - type = types.package; - default = pkgs.copyparty; - defaultText = "pkgs.copyparty"; - description = '' + package = mkPackageOption pkgs "copyparty" { + extraDescription = '' Package of the application to run, exposed for overriding purposes. ''; }; From d5a8a34bcafde04165c4e07e4885b11f6ddd2aff Mon Sep 17 00:00:00 2001 From: Toast <39011842+toast003@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:43:18 +0100 Subject: [PATCH 02/54] NixOS: misc module documentation improvements (#1194) * nixos: use mkPackageOption instead of mkOption for the package option This gets rid of a warning when trying to build a system with documentation.nixos.includeAllModules enabled * nixos: don't include package path in option descriptions See https://github.com/9001/copyparty/pull/1193, this actually fixes the issue * nixos: fix formatting of services.copyparty.volumes..access List will now show up properly on the nixos manual! * nixos: add or improve inline code blocks in option descriptions --- contrib/nixos/modules/copyparty.nix | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/contrib/nixos/modules/copyparty.nix b/contrib/nixos/modules/copyparty.nix index 612672ee..13a8c21c 100644 --- a/contrib/nixos/modules/copyparty.nix +++ b/contrib/nixos/modules/copyparty.nix @@ -79,7 +79,7 @@ in type = types.bool; default = true; description = '' - Make a shell script wrapper called 'copyparty-hash' with all options set here, + Make a shell script wrapper called {command}`copyparty-hash` with all options set here, that launches the hashing cli. ''; }; @@ -114,9 +114,9 @@ in type = types.attrs; description = '' Global settings to apply. - Directly maps to values in the [global] section of the copyparty config. + Directly maps to values in the `[global]` section of the copyparty config. Cannot set "c" or "hist", those are set by this module. - See `${getExe cfg.package} --help` for more details. + See {command}`copyparty --help` for more details. ''; default = { i = "127.0.0.1"; @@ -135,7 +135,7 @@ in globalExtraConfig = mkOption { type = types.str; default = ""; - description = "Appended to the end of the [global] section verbatim. This is useful for flags which are used in a repeating manner (e.g. ipu: 255.255.255.1=user) which can't be repeated in the settings = {} attribute set."; + description = "Appended to the end of the `[global]` section verbatim. This is useful for flags which are used in a repeating manner (e.g. `ipu: 255.255.255.1=user`) which can't be repeated in the settings = {} attribute set."; }; accounts = mkOption { @@ -198,21 +198,21 @@ in Attribute list of permissions and the users to apply them to. The key must be a string containing any combination of allowed permission: - "r" (read): list folder contents, download files - "w" (write): upload files; need "r" to see the uploads - "m" (move): move files and folders; need "w" at destination - "d" (delete): permanently delete files and folders - "g" (get): download files, but cannot see folder contents - "G" (upget): "get", but can see filekeys of their own uploads - "h" (html): "get", but folders return their index.html - "a" (admin): can see uploader IPs, config-reload + * "r" (read): list folder contents, download files + * "w" (write): upload files; need "r" to see the uploads + * "m" (move): move files and folders; need "w" at destination + * "d" (delete): permanently delete files and folders + * "g" (get): download files, but cannot see folder contents + * "G" (upget): "get", but can see filekeys of their own uploads + * "h" (html): "get", but folders return their index.html + * "a" (admin): can see uploader IPs, config-reload For example: "rwmd" The value must be one of: - an account name, defined in `accounts` - a list of account names - "*", which means "any account" + * an account name, defined in `accounts` + * a list of account names + * "*", which means "any account" ''; example = literalExpression '' { @@ -227,7 +227,7 @@ in type = types.attrs; description = '' Attribute list of volume flags to apply. - See `${getExe cfg.package} --help-flags` for more details. + See {command}`copyparty --help-flags` for more details. ''; example = literalExpression '' { From 266489113a82976736476768e31a6924c5be0572 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 12 Jan 2026 18:06:05 +0000 Subject: [PATCH 03/54] fix unlistc* for filevols --- copyparty/httpcli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index f7ff5b62..9a0d8b98 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5434,8 +5434,8 @@ class HttpCli(object): if self.args.have_unlistc: allvols = self.asrv.vfs.all_nodes - rvol = [x for x in rvol if "unlistcr" not in allvols[x[1:-1]].flags] - wvol = [x for x in wvol if "unlistcw" not in allvols[x[1:-1]].flags] + rvol = [x for x in rvol if "unlistcr" not in allvols[x.strip("/")].flags] + wvol = [x for x in wvol if "unlistcw" not in allvols[x.strip("/")].flags] fmt = self.uparam.get("ls", "") if not fmt and self.ua.startswith(("curl/", "fetch")): From bc24604a8388483eabb76ee11a4803b4a6a1034c Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 12 Jan 2026 18:46:47 +0000 Subject: [PATCH 04/54] dl-button: skip folders (would give html) --- copyparty/web/browser.js | 12 ++++++++++-- copyparty/web/tl/chi.js | 1 + copyparty/web/tl/cze.js | 1 + copyparty/web/tl/deu.js | 1 + copyparty/web/tl/epo.js | 1 + copyparty/web/tl/fin.js | 1 + copyparty/web/tl/fra.js | 1 + copyparty/web/tl/grc.js | 1 + copyparty/web/tl/ita.js | 1 + copyparty/web/tl/jpn.js | 1 + copyparty/web/tl/kor.js | 1 + copyparty/web/tl/nld.js | 1 + copyparty/web/tl/nno.js | 1 + copyparty/web/tl/nor.js | 1 + copyparty/web/tl/pol.js | 1 + copyparty/web/tl/por.js | 1 + copyparty/web/tl/rus.js | 1 + copyparty/web/tl/spa.js | 1 + copyparty/web/tl/swe.js | 1 + copyparty/web/tl/tur.js | 1 + copyparty/web/tl/ukr.js | 1 + copyparty/web/tl/vie.js | 1 + 22 files changed, 31 insertions(+), 2 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 1f393127..4bcaa0db 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -360,6 +360,7 @@ if (1) "f_anota": "only {0} of the {1} items were selected;\nto select the full folder, first scroll to the bottom", "f_dls": 'the file links in the current folder have\nbeen changed into download links', + "f_dl_nd": 'skipping folder (use zip/tar download instead):\n', "f_partial": "To safely download a file which is currently being uploaded, please click the file which has the same filename, but without the .PARTIAL file extension. Please press CANCEL or Escape to do this.\n\nPressing OK / Enter will ignore this warning and continue downloading the .PARTIAL scratchfile instead, which will almost definitely give you corrupted data.", @@ -8503,9 +8504,13 @@ var msel = (function () { for (var a = 0, aa = links.length; a < aa; a++) { var qhref = links[a].getAttribute('href'), - href = qhref.split('?')[0].replace(/\/$/, ""), + href = qhref.split('?')[0], item = {}; + if (href.endsWith('/')) { + href = href.slice(0, -1); + item.isd = true; + } item.id = links[a].getAttribute('id'); item.sel = clgot(links[a].closest('tr'), 'sel'); item.vp = href.indexOf('/') !== -1 ? href : vbase + href; @@ -8668,7 +8673,10 @@ var msel = (function () { ev(e); var sel = r.getsel(); for (var a = 0; a < sel.length; a++) - dl_file(sel[a].vp + sel[a].q); + if (sel[a].isd) + toast.warn(7, L.f_dl_nd + esc(sel[a].vp)); + else + dl_file(sel[a].vp + sel[a].q); }; r.render = function () { var tds = QSA('#files tbody td+td+td'), diff --git a/copyparty/web/tl/chi.js b/copyparty/web/tl/chi.js index c06ecad5..e335ed06 100644 --- a/copyparty/web/tl/chi.js +++ b/copyparty/web/tl/chi.js @@ -354,6 +354,7 @@ Ls.chi = { "f_anota": "仅选择了 {0} 个项目,共 {1} 个;\n要选择整个文件夹,请先滚动到底部", //m "f_dls": '当前文件夹中的文件链接已\n更改为下载链接', + "f_dl_nd": '跳过文件夹(请改用 zip/tar 下载):\n', //m "f_partial": "要安全下载正在上传的文件,请点击没有 .PARTIAL 文件扩展名的同名文件。请按取消或 Escape 执行此操作。\n\n按 确定 / Enter 将忽略此警告并继续下载 .PARTIAL 临时文件,这几乎肯定会导致数据损坏。", diff --git a/copyparty/web/tl/cze.js b/copyparty/web/tl/cze.js index 41f7d770..e5b43047 100644 --- a/copyparty/web/tl/cze.js +++ b/copyparty/web/tl/cze.js @@ -358,6 +358,7 @@ Ls.cze = { "f_anota": "pouze {0} z {1} položek bylo vybráno;\npro výběr celé složky nejprve přejděte na konec", "f_dls": 'odkazy na soubory v aktuální složce byly\nzměněny na odkazy ke stažení', + "f_dl_nd": 'přeskakuje se složka (místo toho použijte stažení zip/tar):\n', //m "f_partial": "Pro bezpečné stažení souboru, který se aktuálně nahrává, klikněte prosím na soubor se stejným názvem, ale bez přípony .PARTIAL. Stiskněte prosím Zrušit nebo Escape.\n\nStisknutím OK / Enter ignorujete toto varování a pokračujete ve stahování .PARTIAL dočasného souboru, což téměř jistě vyústí jako poškozená data.", diff --git a/copyparty/web/tl/deu.js b/copyparty/web/tl/deu.js index 80eab254..f040a28a 100644 --- a/copyparty/web/tl/deu.js +++ b/copyparty/web/tl/deu.js @@ -354,6 +354,7 @@ Ls.deu = { "f_anota": "nur {0} der {1} Elemente wurden ausgewählt;\num den gesamten Ordner auszuwählen, zuerst nach unten scrollen", "f_dls": 'die Dateilinks im aktuellen Ordner wurden\nin Downloadlinks geändert', + "f_dl_nd": 'ordner wird übersprungen (bitte zip/tar-download verwenden):\n', //m "f_partial": "Um eine Datei sicher herunterzuladen, die gerade hochgeladen wird, klicke bitte die Datei mit dem gleichen Namen, aber ohne die .PARTIAL-Endung. Bitte drücke Abbrechen oder Escape, um dies zu tun.\n\nWenn du auf OK / Eingabe drückst, ignorierst du diese Warnung und lädst die .PARTIAL-Datei herunter, die ziemlich sicher beschädigte Daten enthält.", diff --git a/copyparty/web/tl/epo.js b/copyparty/web/tl/epo.js index 39a6024c..c532a34c 100644 --- a/copyparty/web/tl/epo.js +++ b/copyparty/web/tl/epo.js @@ -354,6 +354,7 @@ Ls.epo = { "f_anota": "nur {0} de {1} eroj estis elektita;\nrulumi al la malsupro por elekti la tutan dosierujon", "f_dls": 'la ligiloj de dosieroj en ĉi tiu dosierujo estis\nanstataŭigitaj per elŝuto-ligiloj', + "f_dl_nd": 'preterlasante dosierujon (uzu zip/tar-elŝuton anstataŭe):\n', //m "f_partial": "Por sendifekta elŝuto de nune-alŝutata dosiero, elektu dosieron kun sama nomo, sed sen etendaĵo .PARTIAL. Bonvolu uzi la butonon \"Rezigni\" aŭ klavon ESK por fari tion.\n\nSe vi uzas OK / Enter, la provizora dosiero .PARTIAL estos elŝutita, kiu tre probable enhavas nekompletajn datumojn.", diff --git a/copyparty/web/tl/fin.js b/copyparty/web/tl/fin.js index 3ea70587..64080718 100644 --- a/copyparty/web/tl/fin.js +++ b/copyparty/web/tl/fin.js @@ -354,6 +354,7 @@ Ls.fin = { "f_anota": "vain {0} / {1} kohdetta valittiin;\nvalitaksesi koko hakemiston, vieritä ensin loppuun", "f_dls": 'nykyisen hakemiston tiedostolinkit on\nvaihdettu latauslinkeiksi', + "f_dl_nd": 'ohitetaan kansio (käytä zip/tar-latausta sen sijaan):\n', //m "f_partial": "Ladataksesi turvallisesti tiedoston joka on parhaillaan latautumassa, klikkaa tiedostoa jolla on sama nimi mutta ilman .PARTIAL päätettä. Paina PERUUTA tai Escape tehdäksesi tämän.\n\nOK / Enter painaminen sivuuttaa tämän varoituksen ja jatkaa .PARTIAL väliaikaistiedoston lataamista, mikä todennäköisesti antaa sinulle vioittunutta dataa.", diff --git a/copyparty/web/tl/fra.js b/copyparty/web/tl/fra.js index 785eb4fb..a8ab1649 100644 --- a/copyparty/web/tl/fra.js +++ b/copyparty/web/tl/fra.js @@ -354,6 +354,7 @@ Ls.fra = { "f_anota": "seulement {0} des {1} elements sont selectioné;\npour selectioner le dossier entier, fait défiler jusqu'au fond", "f_dls": 'le lien de fichier dans le répertoire actuel\nà été changé en lien de téléchargement', + "f_dl_nd": 'dossier ignoré (utilisez le téléchargement zip/tar à la place):\n', //m "f_partial": "Pour télécharger de façon sécurisée un fichier qui est entrain de se faire téléverser, cliquez sur le fichier qui a le même nom, mais sans l'extension de fichier .PARTIAL. Choisissez ANNULER ou appuiez sur la touche Échap pour faire cela.\n\nAppuyer sur OK / Entrée ignorera cet avertissement et continuera à télécharger le fichier temporaire .PARTIAL à la place, ce qui donnera presque certainement des données corrompues.", diff --git a/copyparty/web/tl/grc.js b/copyparty/web/tl/grc.js index da3f0ae0..94065e9f 100644 --- a/copyparty/web/tl/grc.js +++ b/copyparty/web/tl/grc.js @@ -354,6 +354,7 @@ Ls.grc = { "f_anota": "μόνο {0} από τα {1} αντικείμενα επιλέχθηκαν;\nγια να επιλέξεις ολόκληρο το φάκελο, κύλησε πρώτα μέχρι κάτω", "f_dls": 'οι σύνδεσμοι αρχείων στον τρέχοντα φάκελο έχουν\nμετατραπεί σε συνδέσμους λήψης', + "f_dl_nd": 'παράλειψη φακέλου (χρησιμοποιήστε λήψη zip/tar αντί γι’ αυτό):\n', //m "f_partial": "Για να κατεβάσεις με ασφάλεια ένα αρχείο που ανεβαίνει, κλίκαρε το αρχείο με το ίδιο όνομα, αλλά χωρίς την κατάληξη .PARTIAL. Πάτα Άκυρο ή Escape για να σταματήσεις.\n\nΠάτα Εντάξει / Enter αν αγνοείς την προειδοποίηση και κατέβασε το .PARTIAL αρχείο, που σχεδόν σίγουρα θα είναι κατεστραμμένο.", diff --git a/copyparty/web/tl/ita.js b/copyparty/web/tl/ita.js index 52cf0c12..77a0276b 100644 --- a/copyparty/web/tl/ita.js +++ b/copyparty/web/tl/ita.js @@ -354,6 +354,7 @@ Ls.ita = { "f_anota": "solo {0} dei {1} elementi sono stati selezionati;\nper selezionare l'intera cartella, prima scorri fino in fondo", "f_dls": 'i link dei file nella cartella corrente sono stati\ncambiati in link di download', + "f_dl_nd": 'cartella ignorata (usa invece il download zip/tar):\n', //m "f_partial": "Per scaricare in sicurezza un file che è attualmente in fase di caricamento, clicca il file che ha lo stesso nome, ma senza l'estensione .PARTIAL. Premi ANNULLA o Escape per farlo.\n\nPremendo OK / Invio ignorerai questo avviso e continuerai a scaricare il file .PARTIAL scratch, che quasi sicuramente ti darà dati corrotti.", diff --git a/copyparty/web/tl/jpn.js b/copyparty/web/tl/jpn.js index 6593a2df..2f97a7c5 100644 --- a/copyparty/web/tl/jpn.js +++ b/copyparty/web/tl/jpn.js @@ -354,6 +354,7 @@ Ls.jpn = { "f_anota": "{1}件のアイテムのうち {0}件が選択されました;\nフォルダ全体を選択するには、まず一番下までスクロールします", "f_dls": '現在のフォルダ内のファイルリンクは\nダウンロードリンクに変更されました', + "f_dl_nd": 'フォルダーをスキップしています(代わりに zip/tar ダウンロードを使用してください):\n', //m "f_partial": "現在アップロード中のファイルを安全にダウンロードするには、同じファイル名で.PARTIAL拡張子がないファイルをクリックしてください。これを行うにはキャンセルまたはEscキーを押してください。\n\nOK / Enter を押すとこの警告は無視され、代わりに.PARTIALスクラッチファイルのダウンロードが続行されますが、ほとんどの場合データが破損することになります。", diff --git a/copyparty/web/tl/kor.js b/copyparty/web/tl/kor.js index 9962279d..6bebbd64 100644 --- a/copyparty/web/tl/kor.js +++ b/copyparty/web/tl/kor.js @@ -354,6 +354,7 @@ Ls.kor = { "f_anota": "{1}개 항목 중 {0}개만 선택되었습니다.\n전체 폴더를 선택하려면 먼저 맨 아래로 스크롤하세요.", "f_dls": '현재 폴더의 파일 링크가\n다운로드 링크로 변경되었습니다', + "f_dl_nd": '폴더를 건너뜁니다 (대신 zip/tar 다운로드를 사용하세요):\n', //m "f_partial": "현재 업로드 중인 파일을 안전하게 다운로드하려면, 파일 이름이 같지만 .PARTIAL 확장자가 없는 파일을 클릭하세요. 이 경고를 무시하려면 \"취소\" 또는 ESC를 누르세요.\n\n\"확인\"/Enter를 누르면 이 경고를 무시하고 .PARTIAL 임시 파일을 계속 다운로드하며, 이 경우 거의 확실히 손상된 데이터를 받게 됩니다.", diff --git a/copyparty/web/tl/nld.js b/copyparty/web/tl/nld.js index 4c0aed64..b4287b96 100644 --- a/copyparty/web/tl/nld.js +++ b/copyparty/web/tl/nld.js @@ -354,6 +354,7 @@ Ls.nld = { "f_anota": "Alleen {0} van de {1} items zijn geselecteerd;\nom de volledige map te selecteren, scrol je eerst naar beneden", "f_dls": 'de bestandslinks in de huidige map zijn veranderd in downloadlinks', + "f_dl_nd": 'map wordt overgeslagen (gebruik in plaats daarvan zip/tar-download):\n', //m "f_partial": "Om een bestand dat momenteel wordt geüpload veilig te downloaden, klikt u op het bestand met dezelfde bestandsnaam, maar zonder de bestandsextensie .PARTIAL. Druk op Annuleren of Escape om dit te doen.\n\nAls u op OK / Enter drukt, wordt deze waarschuwing genegeerd en gaat u verder met het downloaden van het gedeeltelijke .PARTIAL scratchbestand, waardoor u vrijwel zeker beschadigde gegevens krijgt.", diff --git a/copyparty/web/tl/nno.js b/copyparty/web/tl/nno.js index 9e7065b4..796ee8f5 100644 --- a/copyparty/web/tl/nno.js +++ b/copyparty/web/tl/nno.js @@ -351,6 +351,7 @@ Ls.nno = { "f_anota": "kun {0} av totalt {1} element blei markert;\nfor å velje alt må du bla åt bunnen av mappa først", "f_dls": 'lenkane i denne mappa er no\nomgjort åt nedlastingsknappar', + "f_dl_nd": 'hoppar over mappe (bruk zip/tar-nedlasting i staden):\n', "f_partial": "For å laste ned ei fil som enda ikkje er ferdig opplasta, klikk på filen som har same filnamn som denne, men uten .PARTIAL på slutten. Da vil serveren passe på at nedlastinga går bra. Derfor anbefalast det sterkt å trykkje AVBRYT eller Escape-tasten.\n\nViss du verkelig ønskjer å laste ned denne .PARTIAL-filen på ein ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høgst sannsynleg motta korrupt data.", diff --git a/copyparty/web/tl/nor.js b/copyparty/web/tl/nor.js index e005fe8d..ad8f8f33 100644 --- a/copyparty/web/tl/nor.js +++ b/copyparty/web/tl/nor.js @@ -351,6 +351,7 @@ Ls.nor = { "f_anota": "kun {0} av totalt {1} elementer ble markert;\nfor å velge alt må du bla til bunnen av mappen først", "f_dls": 'linkene i denne mappen er nå\nomgjort til nedlastningsknapper', + "f_dl_nd": 'hopper over mappe (bruk zip/tar-nedlasting i stedet):\n', "f_partial": "For å laste ned en fil som enda ikke er ferdig opplastet, klikk på filen som har samme filnavn som denne, men uten .PARTIAL på slutten. Da vil serveren passe på at nedlastning går bra. Derfor anbefales det sterkt å trykke AVBRYT eller Escape-tasten.\n\nHvis du virkelig ønsker å laste ned denne .PARTIAL-filen på en ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høyst sannsynlig motta korrupt data.", diff --git a/copyparty/web/tl/pol.js b/copyparty/web/tl/pol.js index 21335b0b..13c3a7de 100644 --- a/copyparty/web/tl/pol.js +++ b/copyparty/web/tl/pol.js @@ -357,6 +357,7 @@ Ls.pol = { "f_anota": "{0} z {1} elementów zostało wybranych;\naby pokazać cały folder, zjedź na dół", "f_dls": 'linki do plików w aktualnym folderze\nzostały zmienione w linki pobierania', + "f_dl_nd": 'pomijanie folderu (użyj zamiast tego pobierania zip/tar):\n', //m "f_partial": "Aby bezpiecznie pobrać plik, który aktualnie jest przesyłany, wybierz plik o tej samej nazwie, lecz bez rozszerzenia .PARTIAL. Żeby to zrobić, naciśnij ANULUJ lub klawisz ESC.\n\nWciśnięcie OK / Enter zignoruje to ostrzeżenie i pobierze plik tymczasowy .PARTIAL, który prawie z pewnością będzie zepsuty", diff --git a/copyparty/web/tl/por.js b/copyparty/web/tl/por.js index 5ece210d..53e54302 100644 --- a/copyparty/web/tl/por.js +++ b/copyparty/web/tl/por.js @@ -354,6 +354,7 @@ Ls.por = { "f_anota": "apenas {0} dos {1} itens foram selecionados;\npara selecionar a pasta inteira, primeiro role para o final", "f_dls": 'os links de arquivo na pasta atual foram\nalterados para links de download', + "f_dl_nd": 'a ignorar pasta (use o download zip/tar em vez disso):\n', //m "f_partial": "Para baixar com segurança um arquivo que está sendo enviado, por favor, clique no arquivo que tem o mesmo nome, mas sem a extensão .PARTIAL. Por favor, pressione CANCELAR ou Escape para fazer isso.\n\nPressionar OK / Enter irá ignorar este aviso e continuar baixando o arquivo temporário .PARTIAL, o que quase certamente lhe dará dados corrompidos.", diff --git a/copyparty/web/tl/rus.js b/copyparty/web/tl/rus.js index 947b1f9a..549921e7 100644 --- a/copyparty/web/tl/rus.js +++ b/copyparty/web/tl/rus.js @@ -354,6 +354,7 @@ Ls.rus = { "f_anota": "только {0} из {1} файлов было выделено;\nчтобы выделить всё папку, отмотайте до низа", "f_dls": 'ссылки на файлы в данной папке были\nзаменены ссылками на скачивание', + "f_dl_nd": 'пропуск папки (используйте загрузку zip/tar вместо этого):\n', //m "f_partial": "Чтобы безопасно скачать файл, который в текущий момент загружается, нажмите на файл с таким же названием, но без расширения .PARTIAL. Пожалуйста, нажмите Отмена или ESC, чтобы сделать это.\n\nПри нажатии OK / Enter, вы скачаете этот временный файл, который с огромной вероятностью содержит лишь неполные данные.", diff --git a/copyparty/web/tl/spa.js b/copyparty/web/tl/spa.js index 7c2ea5db..2267aa87 100644 --- a/copyparty/web/tl/spa.js +++ b/copyparty/web/tl/spa.js @@ -353,6 +353,7 @@ Ls.spa = { "f_anota": "solo {0} de los {1} elementos fueron seleccionados;\npara seleccionar la carpeta completa, primero desplázate hasta el final", "f_dls": "los enlaces a archivos en la carpeta actual se han\nconvertido en enlaces de descarga", + "f_dl_nd": 'omitiendo carpeta (use la descarga zip/tar en su lugar):\n', //m "f_partial": "Para descargar de forma segura un archivo que se está subiendo actualmente, por favor haz clic en el archivo con el mismo nombre, pero sin la extensión .PARTIAL. Por favor, pulsa CANCELAR o Escape para hacer esto.\n\nPulsar ACEPTAR o Intro ignorará esta advertencia y continuará descargando el archivo temporal .PARTIAL, lo que casi con toda seguridad te dará datos corruptos.", diff --git a/copyparty/web/tl/swe.js b/copyparty/web/tl/swe.js index 39009b15..b5d4e8f9 100644 --- a/copyparty/web/tl/swe.js +++ b/copyparty/web/tl/swe.js @@ -354,6 +354,7 @@ Ls.swe = { "f_anota": "endast {0} av {1} objekt valdes;\nför att välja hela mappen, skrolla först till botten av vyn", "f_dls": 'fillänkarna i den öppna mappen har\nbytts till nedladdningslänkar', + "f_dl_nd": 'hoppar över mapp (använd zip/tar-nedladdning istället):\n', //m "f_partial": "För att säkert ladda ner en fil som för tillfället laddas upp, vänligen klicka på filen som har samma filnamn men utan .PARTIAL-filändelsen. Vänligen tryck Avbryt eller Escape för att göra detta.\n\nOm du bortser från denna varning och trycker OK eller Enter kommer den tillfälliga .PARTIAL-filen istället att laddas ner, vilket är nästan garanterat att ge dig korrumperad data.", diff --git a/copyparty/web/tl/tur.js b/copyparty/web/tl/tur.js index 7b0d35cd..3034b283 100644 --- a/copyparty/web/tl/tur.js +++ b/copyparty/web/tl/tur.js @@ -354,6 +354,7 @@ Ls.tur = { "f_anota": "{1} dosyadan sadece {0} tanesi seçildi;\nTüm klasörü seçmek için önce en alta kaydırın.", "f_dls": 'bu klasördeki dosya linkleri\nindirme linklerine dönüştürüldü', + "f_dl_nd": 'klasör atlanıyor (bunun yerine zip/tar indirmesini kullanın):\n', //m "f_partial": "Mevcutta yüklenen bir dosyayı güvenli bir şekilde indirmek için lütfen aynı adlı ama .PARTIAL uzantısına sahip olmayan dosyaya tıklayın. Lütfen bunu yapmak için İPTAL veya Esc tuşuna basın.\n\nTamam / Enter tuşuna basmak, bu uyarıyı yok sayacak ve bunun yerine .PARTIAL geçici dosyasını indirmeye devam edecektir ki bu da elinize bozuk veriler sunacaktır.", diff --git a/copyparty/web/tl/ukr.js b/copyparty/web/tl/ukr.js index db56bdaf..33ee9f0c 100644 --- a/copyparty/web/tl/ukr.js +++ b/copyparty/web/tl/ukr.js @@ -354,6 +354,7 @@ Ls.ukr = { "f_anota": "лише {0} з {1} елементів було вибрано;\nщоб вибрати всю папку, спочатку прокрутіть до низу", "f_dls": 'посилання на файли в поточній папці були\nзмінені на посилання для завантаження', + "f_dl_nd": 'пропуск папки (скористайтеся завантаженням zip/tar замість цього):\n', //m "f_partial": "Щоб безпечно завантажити файл, який зараз завантажується, будь ласка, клацніть на файл, який має таке саме ім'я, але без розширення .PARTIAL. Будь ласка, натисніть Скасувати або Escape, щоб зробити це.\n\nНатиснення Гаразд / Enter проігнорує це попередження і продовжить завантаження .PARTIAL робочого файлу замість цього, що майже напевно дасть вам пошкоджені дані.", diff --git a/copyparty/web/tl/vie.js b/copyparty/web/tl/vie.js index 35a42cc5..2932a44d 100644 --- a/copyparty/web/tl/vie.js +++ b/copyparty/web/tl/vie.js @@ -363,6 +363,7 @@ Ls.vie = { "f_anota": "chỉ {0} trong {1} tệp được chọn;\nđể chọn toàn bộ thư mục, trước tiên hãy kéo xuống cuối", "f_dls": 'những đường dẫn đến tệp trong thư mục này\nđã được chuyển thành đường dẫn tải trực tiếp', + "f_dl_nd": 'bỏ qua thư mục (hãy dùng tải zip/tar thay thế):\n', //m "f_partial": "Để tải an toàn một tệp đang được tải lên, hãy bấm vào tệp có cùng tên nhưng *không* có phần mở rộng .PARTIAL. Hãy nhấn CANCEL hoặc Escape để thực hiện.\n\nNếu nhấn OK / Enter, cảnh báo sẽ bị bỏ qua và bạn sẽ tải tệp tạm .PARTIAL thay vào đó, gần như chắc chắn dẫn đến dữ liệu bị hỏng.", From 8240ef61517cc51f72dd00a26ef4f4c612066c99 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 13 Jan 2026 02:50:32 +0000 Subject: [PATCH 05/54] index xattrs as tags; closes #134 --- README.md | 15 +++++++++++++++ copyparty/__main__.py | 2 ++ copyparty/authsrv.py | 13 +++++++++++++ copyparty/cfg.py | 2 ++ copyparty/mtag.py | 39 ++++++++++++++++++++++++++++++++++++++- copyparty/up2k.py | 20 ++++++++++++-------- tests/util.py | 2 +- 7 files changed, 83 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f153b929..3e79ccb2 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com * [other flags](#other-flags) * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload + * [metadata from xattrs](#metadata-from-xattrs) - unix extended file attributes * [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags * [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/)) * [zeromq](#zeromq) - event-hooks can send zeromq messages @@ -1877,6 +1878,20 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy `--mtag-to` sets the tag-scan timeout; very high default (60 sec) to cater for zfs and other randomly-freezing filesystems. Lower values like 10 are usually safe, allowing for faster processing of tricky files +### metadata from xattrs + +unix extended file attributes can be indexed into the db and made searchable; + +* `--db-xattr user.foo,user.bar` will index the xattrs `user.foo` and `user.bar`, +* `--db-xattr user.foo=foo,user.bar=bar` will index them with the names `foo` and `bar`, +* `--db-xattr ~~user.foo,user.bar` will index everything *except* `user.foo` and `user.bar`, +* `--db-xattr ~~` will index everything + +however note that the tags must also be enabled with `-mte` so here are some complete examples: +* `-e2ts --db-xattr user.foo,user.bar -mte +user.foo,user.bar` +* `-e2ts --db-xattr user.foo=foo,user.bar=bar -mte +foo,bar` + + ## file parser plugins provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index b2186d36..d45f165c 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1781,10 +1781,12 @@ def add_db_metadata(ap): ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning") ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such") ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers") + ap2.add_argument("--db-xattr", metavar="t,t", type=u, default="", help="read file xattrs as metadata tags; [\033[32ma,b\033[0m] reads keys \033[33ma\033[0m and \033[33mb\033[0m as tags \033[33ma\033[0m and \033[33mb\033[0m, [\033[32ma=foo,b=bar\033[0m] reads keys \033[33ma\033[0m and \033[33mb\033[0m as tags \033[33mfoo\033[0m and \033[33mbar\033[0m, [\033[32m~~a,b\033[0m] does everything except \033[33ma\033[0m and \033[33mb\033[0m, [\033[32m~~\033[0m] does everything. NOTE: Each tag must also be enabled with \033[33m-mte\033[0m (volflag=db_xattr)") ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add/replace metadata mapping") ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE) ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH) ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file") + ap2.add_argument("--have-db-xattr", action="store_true", help=argparse.SUPPRESS) def add_txt(ap): diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 59830e1c..0002f2c8 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2591,6 +2591,19 @@ class AuthSrv(object): vol.check_landmarks() + if vol.flags.get("db_xattr"): + self.args.have_db_xattr = True + zs = str(vol.flags["db_xattr"]) + neg = zs.startswith("~~") + if neg: + zs = zs[2:] + zsl = [x.strip() for x in zs.split(",")] + zsl = [x for x in zsl if x] + if neg: + vol.flags["db_xattr_no"] = set(zsl) + else: + vol.flags["db_xattr_yes"] = zsl + # d2d drops all database features for a volume for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]: if not vol.flags.get(grp, False): diff --git a/copyparty/cfg.py b/copyparty/cfg.py index d33287e9..163fd4ad 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -101,6 +101,7 @@ def vf_vmap() -> dict[str, str]: "chmod_d", "chmod_f", "dbd", + "db_xattr", "du_who", "epilogues", "ufavico", @@ -286,6 +287,7 @@ flagcats = { "dotsrch": "show dotfiles in search results", "nodotsrch": "hide dotfiles in search results (default)", "srch_excl": "exclude search results with URL matching this regex", + "db_xattr=user.foo,user.bar": "index file xattrs as media-tags", }, 'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': { "mte=artist,title": "media-tags to index/display", diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 41c28a09..cedc0c27 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -465,6 +465,8 @@ class MTag(object): "ffprobe" if args.no_mutagen or (HAVE_FFPROBE and EXE) else "mutagen" ) self.can_ffprobe = HAVE_FFPROBE and not args.no_mtag_ff + self.read_xattrs = args.have_db_xattr + self.get = self._get_xattr if self.read_xattrs else self._get_main mappings = args.mtm or_ffprobe = " or FFprobe" @@ -486,7 +488,13 @@ class MTag(object): msg = "found FFprobe but it was disabled by --no-mtag-ff" self.log(msg, c=3) + if self.read_xattrs and not self.usable: + t = "don't have the necessary dependencies to read conventional media tags, but will read xattrs" + self.log(t) + self.usable = True + if not self.usable: + self._get = None if EXE: t = "copyparty.exe cannot use mutagen; need ffprobe.exe to read media tags: " self.log(t + FFMPEG_URL) @@ -645,7 +653,36 @@ class MTag(object): return r1 - def get(self, abspath: str) -> dict[str, Union[str, float]]: + def _get_xattr( + self, abspath: str, vf: dict[str, Any] + ) -> dict[str, Union[str, float]]: + ret = self._get_main(abspath, vf) if self._get else {} + if "db_xattr_no" in vf: + try: + neg = vf["db_xattr_no"] + zsl = os.listxattr(abspath) + zsl = [x for x in zsl if x not in neg] + for xattr in zsl: + zb = os.getxattr(abspath, xattr) + ret[xattr] = zb.decode("utf-8", "replace") + except: + self.log("failed to read xattrs from [%s]\n%s", abspath, min_ex(), 3) + elif "db_xattr_yes" in vf: + for xattr in vf["db_xattr_yes"]: + if "=" in xattr: + xattr, name = xattr.split("=", 1) + else: + name = xattr + try: + zs = os.getxattr(abspath, xattr) + ret[name] = zs.decode("utf-8", "replace") + except: + pass + return ret + + def _get_main( + self, abspath: str, vf: dict[str, Any] + ) -> dict[str, Union[str, float]]: ext = abspath.split(".")[-1].lower() if ext not in self.args.au_unpk: return self._get(abspath) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 06696cf4..3d0be53c 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -130,6 +130,7 @@ class Mpqe(object): self, mtp: dict[str, MParser], entags: set[str], + vf: dict[str, Any], w: str, abspath: str, oth_tags: dict[str, Any], @@ -137,6 +138,7 @@ class Mpqe(object): # mtp empty = mtag self.mtp = mtp self.entags = entags + self.vf = vf self.w = w self.abspath = abspath self.oth_tags = oth_tags @@ -2199,7 +2201,7 @@ class Up2k(object): abspath = djoin(ptop, rd, fn) self.pp.msg = "c%d %s" % (nq, abspath) if not mpool: - n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un) + n_tags = self._tagscan_file(cur, entags, flags, w, abspath, ip, at, un) else: oth_tags = {} if ip: @@ -2209,7 +2211,7 @@ class Up2k(object): if un: oth_tags["up_by"] = un - mpool.put(Mpqe({}, entags, w, abspath, oth_tags)) + mpool.put(Mpqe({}, entags, flags, w, abspath, oth_tags)) with self.mutex: n_tags = len(self._flush_mpool(cur)) @@ -2316,9 +2318,10 @@ class Up2k(object): return entags = self.entags[ptop] + vf = self.flags[ptop] parsers = {} - for parser in self.flags[ptop]["mtp"]: + for parser in vf["mtp"]: try: parser = MParser(parser) except: @@ -2393,7 +2396,7 @@ class Up2k(object): if un: oth_tags["up_by"] = un - jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags)) + jobs.append(Mpqe(parsers, set(), vf, w, abspath, oth_tags)) in_progress[w] = True with self.mutex: @@ -2524,7 +2527,7 @@ class Up2k(object): return for _ in range(mpool.maxsize): - mpool.put(Mpqe({}, set(), "", "", {})) + mpool.put(Mpqe({}, set(), {}, "", "", {})) mpool.join() @@ -2543,7 +2546,7 @@ class Up2k(object): t = "tag-thr: {}({})" self.log(t.format(self.mtag.backend, qe.abspath), "90") - tags = self.mtag.get(qe.abspath) if st.st_size else {} + tags = self.mtag.get(qe.abspath, qe.vf) if st.st_size else {} else: if self.args.mtag_vv: t = "tag-thr: {}({})" @@ -2576,6 +2579,7 @@ class Up2k(object): self, write_cur: "sqlite3.Cursor", entags: set[str], + vf: dict[str, Any], wark: str, abspath: str, ip: str, @@ -2594,7 +2598,7 @@ class Up2k(object): return 0 try: - tags = self.mtag.get(abspath) if st.st_size else {} + tags = self.mtag.get(abspath, vf) if st.st_size else {} except Exception as ex: self._log_tag_err("", abspath, ex) return 0 @@ -5404,7 +5408,7 @@ class Up2k(object): # self.log("\n " + repr([ptop, rd, fn])) abspath = djoin(ptop, rd, fn) try: - tags = self.mtag.get(abspath) if sz else {} + tags = self.mtag.get(abspath, self.flags[ptop]) if sz else {} ntags1 = len(tags) parsers = self._get_parsers(ptop, tags, abspath) if self.args.mtag_vv: diff --git a/tests/util.py b/tests/util.py index 931d98f0..bc66512c 100644 --- a/tests/util.py +++ b/tests/util.py @@ -164,7 +164,7 @@ class Cfg(Namespace): ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ka.update(**{k: 0 for k in ex.split()}) - 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" + ex = "ah_alg bname chdir chmod_f chpw_db db_xattr 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 = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rss_fmt_d rss_fmt_t spinner" From c46cd7f57a8ae3b121866485c91ec078c4dd970e Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 13 Jan 2026 20:38:46 +0000 Subject: [PATCH 06/54] add html id for css; closes #1200 --- copyparty/web/browser.html | 2 +- copyparty/web/idp.html | 2 +- copyparty/web/md.html | 6 +++++- copyparty/web/mde.html | 6 +++++- copyparty/web/msg.html | 2 +- copyparty/web/rups.html | 2 +- copyparty/web/shares.html | 2 +- copyparty/web/splash.html | 2 +- copyparty/web/svcs.html | 2 +- 9 files changed, 17 insertions(+), 9 deletions(-) diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index e8a91c5a..b372cd58 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -1,5 +1,5 @@ - + diff --git a/copyparty/web/idp.html b/copyparty/web/idp.html index 9db4b88c..1e87ff93 100644 --- a/copyparty/web/idp.html +++ b/copyparty/web/idp.html @@ -1,5 +1,5 @@ - + diff --git a/copyparty/web/md.html b/copyparty/web/md.html index d1a186ec..ac17db2f 100644 --- a/copyparty/web/md.html +++ b/copyparty/web/md.html @@ -1,4 +1,7 @@ - + + + + 📝 {{ title }} @@ -11,6 +14,7 @@ {%- endif %} {{ html_head }} +
diff --git a/copyparty/web/mde.html b/copyparty/web/mde.html index 47bf8d0c..82a8cf65 100644 --- a/copyparty/web/mde.html +++ b/copyparty/web/mde.html @@ -1,4 +1,7 @@ - + + + + 📝 {{ title }} @@ -10,6 +13,7 @@ {{ html_head }} +
diff --git a/copyparty/web/msg.html b/copyparty/web/msg.html index f5d3edc9..30f53d55 100644 --- a/copyparty/web/msg.html +++ b/copyparty/web/msg.html @@ -1,5 +1,5 @@ - + diff --git a/copyparty/web/rups.html b/copyparty/web/rups.html index 544c1ac1..c657d09b 100644 --- a/copyparty/web/rups.html +++ b/copyparty/web/rups.html @@ -1,5 +1,5 @@ - + diff --git a/copyparty/web/shares.html b/copyparty/web/shares.html index fcb53cad..c7da235e 100644 --- a/copyparty/web/shares.html +++ b/copyparty/web/shares.html @@ -1,5 +1,5 @@ - + diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 4f95eff1..f5332c61 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -1,5 +1,5 @@ - + diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index 98348981..e5eeb893 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -1,5 +1,5 @@ - + From 66750391ae71cb284f61d8c8e6de3ec807714843 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 14 Jan 2026 23:46:35 +0000 Subject: [PATCH 07/54] fix jumpvol ?ls --- copyparty/httpcli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 9a0d8b98..2afd57bb 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6450,11 +6450,12 @@ class HttpCli(object): if self.args.have_unlistc: rvol = [x for x in rvol if "unlistcr" not in allvols[x].flags] wvol = [x for x in wvol if "unlistcw" not in allvols[x].flags] - vols = list(set(rvol + wvol)) + vols = [(x, allvols[x]) for x in list(set(rvol + wvol))] if self.vpath: zs = "%s/" % (self.vpath,) - vols = [x[len(zs) :] for x in vols if x.startswith(zs)] - vols = [x.split("/", 1)[0] for x in vols if x] + vols = [(x[len(zs) :], y) for x, y in vols if x.startswith(zs)] + vols = [(x.split("/", 1)[0], y) for x, y in vols] + vols = [x for x in vols if x[0]] if not vols and self.vpath: return self.tx_404(True) dirs = [ @@ -6467,9 +6468,9 @@ class HttpCli(object): "tags": e_d, "dt": 0, "name": 0, - "perms": allvols[x].get_perms("", self.uname), + "perms": vn.get_perms("", self.uname), } - for x in sorted(vols) + for x, vn in sorted(vols) ] ls = { "dirs": dirs, From 04f612ff637c35782fec488097667ce970d24209 Mon Sep 17 00:00:00 2001 From: hackysphere Date: Wed, 14 Jan 2026 17:53:07 -0600 Subject: [PATCH 08/54] rcm in search results; closes #1202 (#1198) * fix: make right-click menu work in search view * fix: allow for markdown files to be opened in viewer when in search view --------- Co-authored-by: ed --- copyparty/web/browser.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 4bcaa0db..6c0eb92b 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -9500,9 +9500,10 @@ var rcm = (function () { elem: null, type: null, path: null, + dpath: null, url: null, id: null, - relpath: null, + fname: null, no_dsel: false }; var selFile = jcp(nsFile); @@ -9564,9 +9565,9 @@ var rcm = (function () { a.target = selFile.type == "dir" ? '' : '_blank'; a.click(); break; - case 'ply': selFile.type == 'gf' ? thegrid.imshow(selFile.relpath) : play('f-' + selFile.id); break; + case 'ply': selFile.type == 'gf' ? thegrid.imshow(selFile.name) : play('f-' + selFile.id); break; case 'pla': play('f-' + selFile.id); break; - case 'txt': location = '?doc=' + selFile.relpath; break; + case 'txt': location = selFile.dpath + '?doc=' + selFile.name; break; case 'md': location = selFile.path + (has(selFile.path, '?') ? '&v' : '?v'); break; case 'cpl': cliptxt(selFile.url, function() {toast.ok(2, L.clipped)}); break; case 'dl': ebi('seldl').click(); break; @@ -9599,20 +9600,27 @@ var rcm = (function () { var ref = ebi(target.getAttribute('ref')); file = ref && ref.closest('#files tbody tr'); } - if (file) { + var fa = file && file.children[1].querySelector('a[id]'); + if (fa) { selFile.no_dsel = clgot(file, "sel"); clmod(file, "sel", true); selFile.elem = file; - - selFile.url = file.children[1].firstChild.href; + selFile.url = fa.href; selFile.path = basenames(selFile.url).replace(/(&|\?)v/, ''); - selFile.relpath = selFile.path.split('/').slice(-1)[0].split("?")[0]; - if (noq_href(file.children[1].firstChild).endsWith("/")) + var url = selFile.url.split("?")[0], + vsp = vsplit(url); + selFile.dpath = vsp[0]; + selFile.name = vsp[1]; + if (url.endsWith("/")) selFile.type = "dir"; else { var lead = file.firstChild.firstChild; - selFile.id = lead.id.split('-')[1]; - selFile.type = lead.innerHTML[0] == '(' ? 'gf' : lead.id.split('-')[0]; + if (lead.id === undefined) + selFile.type = "tf"; + else { + selFile.id = lead.id.split('-')[1]; + selFile.type = lead.innerHTML[0] == '(' ? 'gf' : lead.id.split('-')[0]; + } } } } @@ -9627,7 +9635,7 @@ var rcm = (function () { clmod(ebi('rpla'), 'hide', selFile.type != 'gf'); clmod(ebi('rtxt'), 'hide', !selFile.id); clmod(ebi('rs1'), 'hide', !selFile.path); - clmod(ebi('rmd'), 'hide', !selFile.id || selFile.relpath.slice(-3) != ".md"); + clmod(ebi('rmd'), 'hide', selFile.name.slice(-3) != ".md"); clmod(ebi('rcpl'), 'hide', !selFile.path); clmod(ebi('rdl'), 'hide', !has_sel); clmod(ebi('rzip'), 'hide', !has_sel); From d32704ed3700940e6284eedacf174236c26cc337 Mon Sep 17 00:00:00 2001 From: hackysphere Date: Thu, 15 Jan 2026 02:39:03 -0600 Subject: [PATCH 09/54] rcm: search-results fix (#1206) * fix: show rcm on empty space and make search header not selectable --------- Co-authored-by: ed --- copyparty/web/browser.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 6c0eb92b..b7b583d2 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -9503,7 +9503,7 @@ var rcm = (function () { dpath: null, url: null, id: null, - fname: null, + name: null, no_dsel: false }; var selFile = jcp(nsFile); @@ -9601,7 +9601,7 @@ var rcm = (function () { file = ref && ref.closest('#files tbody tr'); } var fa = file && file.children[1].querySelector('a[id]'); - if (fa) { + if (fa && fa.id != 'unsearch') { selFile.no_dsel = clgot(file, "sel"); clmod(file, "sel", true); selFile.elem = file; @@ -9635,7 +9635,7 @@ var rcm = (function () { clmod(ebi('rpla'), 'hide', selFile.type != 'gf'); clmod(ebi('rtxt'), 'hide', !selFile.id); clmod(ebi('rs1'), 'hide', !selFile.path); - clmod(ebi('rmd'), 'hide', selFile.name.slice(-3) != ".md"); + clmod(ebi('rmd'), 'hide', !selFile.name || selFile.name.slice(-3) != ".md"); clmod(ebi('rcpl'), 'hide', !selFile.path); clmod(ebi('rdl'), 'hide', !has_sel); clmod(ebi('rzip'), 'hide', !has_sel); @@ -9666,10 +9666,11 @@ var rcm = (function () { } ebi('wrap').oncontextmenu = function(e) { - r.hide(true); if (!r.enabled || e.shiftKey || (r.double && menu.style.display)) { + r.hide(true); return true; } + r.hide(true); if (selFile.elem && !selFile.no_dsel) { clmod(selFile.elem, "sel", false); msel.selui(); From 5b3191b4959ba5f4511ef234acc4e8c6bcb0232e Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 15 Jan 2026 17:56:55 +0000 Subject: [PATCH 10/54] add warning about traefik bug (#1205) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e79ccb2..e20b2aba 100644 --- a/README.md +++ b/README.md @@ -2250,7 +2250,7 @@ example webserver / reverse-proxy configs: * [lighttpd subdomain](contrib/lighttpd/subdomain.conf) -- entire domain/subdomain * [lighttpd subpath](contrib/lighttpd/subpath.conf) -- location-based (not optimal, but in case you need it) * [nginx config](contrib/nginx/copyparty.conf) -- recommended -* [traefik config](contrib/traefik/copyparty.yaml) +* [traefik config](contrib/traefik/copyparty.yaml) -- only use v3.6.7 or newer [due to CVE-2025-66490](https://github.com/9001/copyparty/issues/1205) ### real-ip From bef07720d9b55b0f8d3fbe1451c85399bc77a931 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 15 Jan 2026 19:19:27 +0000 Subject: [PATCH 11/54] dedup: use chmod/chown volflags; closes #1203 --- copyparty/up2k.py | 23 ++++++++++++++++++----- copyparty/util.py | 8 ++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 3d0be53c..81c221f1 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -61,6 +61,8 @@ from .util import ( s3dec, s3enc, sanitize_fn, + set_ap_perms, + set_fperms, sfsenc, spack, statdir, @@ -3546,7 +3548,7 @@ class Up2k(object): if self.args.nw: return - linked = False + linked = 0 try: if rm and bos.path.exists(dst): wunlink(self.log, dst, flags) @@ -3560,6 +3562,8 @@ class Up2k(object): try: with open(fsenc(src), "rb") as fi, open(fsenc(dst), "wb") as fo: fcntl.ioctl(fo.fileno(), fcntl.FICLONE, fi.fileno()) + if "fperms" in flags: + set_fperms(fo, flags) except: if bos.path.exists(dst): wunlink(self.log, dst, flags) @@ -3597,7 +3601,7 @@ class Up2k(object): try: if "hardlink" in flags: os.link(fsenc(absreal(src)), fsenc(dst)) - linked = True + linked = 2 except Exception as ex: self.log("cannot hardlink: " + repr(ex)) if "hardlinkonly" in flags: @@ -3616,7 +3620,7 @@ class Up2k(object): else: os.symlink(fsenc(lsrc), fsenc(ldst)) - linked = True + linked = 1 except Exception as ex: if str(ex) != "reflink": self.log("cannot link; creating copy: " + repr(ex)) @@ -3630,8 +3634,11 @@ class Up2k(object): raise Exception(t % (src, fsrc, dst)) trystat_shutil_copy2(self.log, fsenc(csrc), fsenc(dst)) - if lmod and (not linked or SYMTIME): - bos.utime_c(self.log, dst, int(lmod), False) + if linked < 2 and (linked < 1 or SYMTIME): + if lmod: + bos.utime_c(self.log, dst, int(lmod), False) + if "fperms" in flags: + set_ap_perms(dst, flags) def handle_chunks( self, ptop: str, wark: str, chashes: list[str] @@ -4507,6 +4514,8 @@ class Up2k(object): bos.utime(dabs, times, False) except: pass + if "fperms" in dvn.flags: + set_ap_perms(dabs, dvn.flags) if xac: runhook( @@ -4792,6 +4801,8 @@ class Up2k(object): wunlink(self.log, sabs, svn.flags) else: atomic_move(self.log, sabs, dabs, svn.flags) + if svn != dvn and "fperms" in dvn.flags: + set_ap_perms(dabs, dvn.flags) except OSError as ex: if ex.errno != errno.EXDEV: @@ -4825,6 +4836,8 @@ class Up2k(object): bos.utime(dabs, times, False) except: pass + if "fperms" in dvn.flags: + set_ap_perms(dabs, dvn.flags) wunlink(self.log, sabs, svn.flags) diff --git a/copyparty/util.py b/copyparty/util.py index 05cd4b02..3cbdf3d1 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -2721,6 +2721,14 @@ def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> os.fchown(fno, vf["uid"], vf["gid"]) +def set_ap_perms(ap: str, vf: dict[str, Any]) -> None: + zb = fsenc(ap) + if "chmod_f" in vf: + os.chmod(zb, vf["chmod_f"]) + if "chown" in vf: + os.chown(zb, vf["uid"], vf["gid"]) + + def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes: try: return shutil.copy2(src, dst) From 930e8642399444d95aa028004873fb1622c2eaeb Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 15 Jan 2026 22:56:10 +0000 Subject: [PATCH 12/54] idxh: ensure trailing slash --- copyparty/httpcli.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 2afd57bb..b7e74c6b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -161,6 +161,7 @@ H_CONN_CLOSE = "Connection: Close" RSS_SORT = {"m": "mt", "u": "at", "n": "fn", "s": "sz"} ACODE2_FMT = set(["opus", "owa", "caf", "mp3", "flac", "wav"]) +IDX_HTML = set(["index.htm", "index.html"]) A_FILE = os.stat_result( (0o644, -1, -1, 1, 1000, 1000, 8, 0x39230101, 0x39230101, 0x39230101) @@ -6723,7 +6724,7 @@ class HttpCli(object): vrem = vjoin(vrem, fn) abspath = ap2 break - elif self.vpath.rsplit("/", 1)[-1] in ("index.htm", "index.html"): + elif self.vpath.rsplit("/", 1)[-1] in IDX_HTML: fk_pass = True if not is_dir and (self.can_read or self.can_get): @@ -7093,13 +7094,16 @@ class HttpCli(object): and "v" not in self.uparam and not is_opds ): - idx_html = set(["index.htm", "index.html"]) for item in files: - if item["name"] in idx_html: + if item["name"] in IDX_HTML: # do full resolve in case of shadowed file vp = vjoin(self.vpath.split("?")[0], item["name"]) vn, rem = self.asrv.vfs.get(vp, self.uname, True, False) ap = vn.canonical(rem) + if not self.trailing_slash and bos.path.isfile(ap): + return self.redirect( + self.vpath + "/", flavor="redirecting to", use302=True + ) return self.tx_file(ap) # is no-cache if icur: From 3e3228e0f63c0911fcbd13904706e5bc928bb190 Mon Sep 17 00:00:00 2001 From: exci <76759714+icxes@users.noreply.github.com> Date: Fri, 16 Jan 2026 02:52:19 +0200 Subject: [PATCH 13/54] add drag-selection to gridview (#1212) --- copyparty/web/browser.css | 9 +++ copyparty/web/browser.js | 149 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 53b59bff..ee63c43c 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -4168,4 +4168,13 @@ html.e #detree { border-color: var(--a); border-radius: .2em; padding: .2em .3em; +} + +.selbox { + position: fixed; + border: 1px solid var(--btn-1h-bg); + background-color: var(--btn-1h-bg); + background-color: rgb(from var(--btn-1h-bg) r g b / 0.5); + pointer-events: none; + z-index: 99; } \ No newline at end of file diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index b7b583d2..9ecf276e 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -231,6 +231,7 @@ if (1) "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', + "ct_dsel": 'use drag selection in grid-view">dsel', "ct_dl": 'force download (don\'t display inline) when a file is clicked">dl', "ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯', "ct_dots": 'show hidden files (if server permits)">dotfiles', @@ -910,6 +911,7 @@ ebi('op_cfg').innerHTML = ( ' ' + L.ct_grid + '\n' + ' ℹ️ tooltips', "ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', - "ct_dsel": 'use drag selection in grid-view">dsel', + "ct_dsel": 'use drag-selection in grid-view">dsel', "ct_dl": 'force download (don\'t display inline) when a file is clicked">dl', "ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯', "ct_dots": 'show hidden files (if server permits)">dotfiles', @@ -6747,7 +6747,7 @@ var treectl = (function () { bcfg_bind(r, 'idxh', 'idxh', idxh, setidxh); bcfg_bind(r, 'dyn', 'dyntree', true, onresize); bcfg_bind(r, 'csel', 'csel', dgsel); - bcfg_bind(r, 'dsel', 'dsel', false); + bcfg_bind(r, 'dsel', 'dsel', !MOBILE); bcfg_bind(r, 'dlni', 'dlni', dlni, resort); bcfg_bind(r, 'dots', 'dotfiles', see_dots, function (v) { r.goto(); @@ -9763,16 +9763,13 @@ function reload_browser() { var is_drag = false; var startx, starty; var selbox = null; - + var ttimer = null; var lpdelay = 400; var mvthresh = 10; function unbox() { - var boxes = QSA('.selbox'); - for (var el of boxes) { - el.remove(); - } + qsr('.selbox'); selbox = null; is_drag = false; is_selma = false; @@ -9847,8 +9844,8 @@ function reload_browser() { } if (!is_drag || !selbox) return; - - if (e.cancelable) e.preventDefault(); + + ev(e); var width = Math.abs(pos.x - startx); var height = Math.abs(pos.y - starty); @@ -9859,6 +9856,9 @@ function reload_browser() { selbox.style.height = height + 'px'; selbox.style.left = left + 'px'; selbox.style.top = top + 'px'; + + if (IE && window.getSelection) + window.getSelection().removeAllRanges(); } function sel_end(e) { @@ -9871,15 +9871,13 @@ function reload_browser() { var sbrect = selbox.getBoundingClientRect(); var faf = QSA('#ggrid a'); - for (var el of faf) { - if (bob(sbrect, el.getBoundingClientRect())) { - sel_toggle(el); - } - }; + for (var a = 0, aa = faf.length; a < aa; a++) + if (bob(sbrect, faf[a].getBoundingClientRect())) + sel_toggle(faf[a]); msel.selui(); } - + unbox(); document.body.style.userSelect = 'auto'; } @@ -9899,7 +9897,7 @@ function reload_browser() { } }); } - + dsel_init(); })(); diff --git a/copyparty/web/tl/fin.js b/copyparty/web/tl/fin.js index 64080718..a680a04f 100644 --- a/copyparty/web/tl/fin.js +++ b/copyparty/web/tl/fin.js @@ -226,6 +226,7 @@ Ls.fin = { "ct_ttips": '◔ ◡ ◔">ℹ️ vihjelaatikot', "ct_thumb": 'valitse kuvakkeiden / pienoiskuvien välillä kuvanäkymässä $NPikanäppäin: T">🖼️ pienoiskuvat', "ct_csel": 'käytä CTRL ja SHIFT tiedostojen valintaan kuvanäkymässä">valitse', + "ct_dsel": 'käytä aluevalintaa tiedostojen valintaan kuvanäkymässä">aluevalinta', "ct_dl": 'pakota lataus (älä näytä upotettuna), kun tiedostoa napsautetaan">dl', //m "ct_ihop": 'kun kuvakatselin suljetaan, vieritä alas viimeksi katsottuun tiedostoon">g⮯', "ct_dots": 'näytä piilotetut tiedostot (jos palvelin sallii)">piilotiedostot', diff --git a/copyparty/web/tl/nor.js b/copyparty/web/tl/nor.js index ad8f8f33..128d2e3e 100644 --- a/copyparty/web/tl/nor.js +++ b/copyparty/web/tl/nor.js @@ -223,6 +223,7 @@ Ls.nor = { "ct_ttips": 'vis hjelpetekst ved å holde musen over ting">ℹ️ tips', "ct_thumb": 'vis miniatyrbilder istedenfor ikoner$NSnarvei: T">🖼️ bilder', "ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk', + "ct_dsel": 'marker filer med klikk-og-dra i ikonvisning">dsel', "ct_dl": 'last ned filer (ikke vis i nettleseren)">dl', "ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯', "ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig', diff --git a/scripts/tl.js b/scripts/tl.js index 54a1cc37..0b80c69c 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -255,6 +255,7 @@ Ls.hmn = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', + "ct_dsel": 'use drag-selection in grid-view">dsel', "ct_dl": 'force download (don\'t display inline) when a file is clicked">dl', "ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯', "ct_dots": 'show hidden files (if server permits)">dotfiles', @@ -293,6 +294,7 @@ Ls.hmn = { "cdt_ask": "when scrolling to the bottom,$Ninstead of loading more files,$Nask what to do", "cdt_hsort": "how many sorting rules (<code>,sorthref</code>) to include in media-URLs. Setting this to 0 will also ignore sorting-rules included in media links when clicking them", "cdt_ren": "enable custom right-click menu, you can still access the regular menu by pressing the shift key and right-clicking", + "cdt_rdb": "show the regular right-click menu when the custom one is already open and right-clicking again", "tt_entree": "show navpane (directory tree sidebar)$NHotkey: B", "tt_detree": "show breadcrumbs$NHotkey: B", @@ -383,6 +385,7 @@ Ls.hmn = { "f_anota": "only {0} of the {1} items were selected;\nto select the full folder, first scroll to the bottom", "f_dls": 'the file links in the current folder have\nbeen changed into download links', + "f_dl_nd": 'skipping folder (use zip/tar download instead):\n', "f_partial": "To safely download a file which is currently being uploaded, please click the file which has the same filename, but without the .PARTIAL file extension. Please press CANCEL or Escape to do this.\n\nPressing OK / Enter will ignore this warning and continue downloading the .PARTIAL scratchfile instead, which will almost definitely give you corrupted data.", @@ -674,7 +677,7 @@ Ls.hmn = { "rc_ply": "play", "rc_pla": "play as audio", "rc_txt": "open in textfile viewer", - "rc_md": "open in text editor", + "rc_md": "open in markdown viewer", "rc_dl": "download", "rc_zip": "download as archive", "rc_cpl": "copy link", @@ -682,6 +685,7 @@ Ls.hmn = { "rc_cut": "cut", "rc_cpy": "copy", "rc_pst": "paste", + "rc_rnm": "rename", "rc_nfo": "new folder", "rc_nfi": "new file", "rc_sal": "select all", From 72c59405e7a49686389414913787245aa8e66164 Mon Sep 17 00:00:00 2001 From: exci <76759714+icxes@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:17:01 +0200 Subject: [PATCH 15/54] dsel: fix bugs, improve selection (#1214) * additive/subtractive select (hold shift/alt while releasing LMB) * mobile support --- copyparty/web/browser.js | 239 ++++++++++++++++++++------------------- 1 file changed, 123 insertions(+), 116 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index bf9aef00..25961847 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -9759,145 +9759,152 @@ function reload_browser() { } (function() { - var is_selma = false; - var is_drag = false; - var startx, starty; - var selbox = null; + var is_selma = false; + var dragging = false; - var ttimer = null; - var lpdelay = 400; - var mvthresh = 10; + var startx, starty; + var fwrap = null; + var selbox = null; + var ttimer = null; - function unbox() { - qsr('.selbox'); - selbox = null; - is_drag = false; - is_selma = false; - } + var lpdelay = 250; + var mvthresh = 10; - function getpp(e) { - if (e.touches && e.touches.length > 0) { - return { x: e.touches[0].clientX, y: e.touches[0].clientY }; - } - return { x: e.clientX, y: e.clientY }; - } + function unbox() { + qsr('.selbox'); + ebi('gfiles').style.removeProperty('pointer-events') + ebi('wrap').style.removeProperty('user-select') + + if (selbox) { + console.log(selbox) + window.getSelection().removeAllRanges(); + } + + is_selma = false; + dragging = false; + fwrap = null; + selbox = null; + ttimer = null; + } - function sel_toggle(el) { - clmod(el, 'sel', 't'); - var eref = el.getAttribute('ref'); - if (eref) { - var ehidden = ebi(eref); - if (ehidden) { - var tr = ehidden.closest('tr'); - if (tr) clmod(tr, 'sel', 't'); - } - } - } + function getpp(e) { + var touch = (e.touches && e.touches[0]) || e; + return { x: touch.clientX, y: touch.clientY }; + } - function bob(rect1, rect2) { - return !(rect1.right < rect2.left || rect1.left > rect2.right || - rect1.bottom < rect2.top || rect1.top > rect2.bottom); - } + function sel_toggle(el, m) { + clmod(el, 'sel', m); + var eref = el.getAttribute('ref'); + if (eref) { + var ehidden = ebi(eref); + if (ehidden) { + var tr = ehidden.closest('tr'); + if (tr) clmod(tr, 'sel', m); + } + } + } - function sel_start(e) { + function bob(b1, b2) { + return !(b1.right < b2.left || b1.left > b2.right || + b1.bottom < b2.top || b1.top > b2.bottom); + } + + function sel_start(e) { + if (e.button !== 0 && e.type !== 'touchstart') return; if (!thegrid.en || !treectl.dsel) return; + if (e.target.closest('#widget,#ops,.opview,.doc')) return; - var pos = getpp(e); - startx = pos.x; - starty = pos.y; + if (e.target.closest('#gfiles')) + ebi('gfiles').style.userSelect = "none" - if (e.type === 'mousedown') { - if (e.button !== 0) { - unbox(); - return; - } - is_selma = true; - start_drag(pos); - } - else if (e.type === 'touchstart') { - ttimer = setTimeout(function() { - is_selma = true; - start_drag(pos); - }, lpdelay); - } - } + var pos = getpp(e); + startx = pos.x; + starty = pos.y; + is_selma = true; + ttimer = null; + + if (e.type === 'touchstart') { + ttimer = setTimeout(function() { + ttimer = null; + start_drag(); + }, lpdelay); + } + } + + function start_drag() { + if (dragging) return; - function start_drag(pos) { - is_drag = true; - selbox = document.createElement('div'); - selbox.className = 'selbox'; - document.body.appendChild(selbox); - document.body.style.userSelect = 'none'; - } + dragging = true; + selbox = document.createElement('div'); + selbox.className = 'selbox'; + document.body.appendChild(selbox); - function sel_move(e) { - if (!treectl.dsel) return; + ebi('gfiles').style.pointerEvents = 'none'; + } + + function sel_move(e) { + if (!is_selma) return; + var pos = getpp(e); + var dist = Math.sqrt(Math.pow(pos.x - startx, 2) + Math.pow(pos.y - starty, 2)); - var pos = getpp(e); + if (e.type === 'touchmove' && ttimer) { + if (dist > mvthresh) { + clearTimeout(ttimer); + ttimer = null; + is_selma = false; + } + return; + } + if (!dragging && dist > mvthresh && !window.getSelection().toString()) { + if (fwrap = e.target.closest('#wrap')) + fwrap.style.userSelect = 'none'; + else return; + start_drag(); + } - if (ttimer && !is_drag) { - var dist = Math.sqrt(Math.pow(pos.x - startx, 2) + Math.pow(pos.y - starty, 2)); - if (dist > mvthresh) { - clearTimeout(ttimer); - ttimer = null; - } - } + if (!dragging || !selbox) return; + ev(e); - if (!is_drag || !selbox) return; + selbox.style.width = Math.abs(pos.x - startx) + 'px'; + selbox.style.height = Math.abs(pos.y - starty) + 'px'; + selbox.style.left = Math.min(pos.x, startx) + 'px'; + selbox.style.top = Math.min(pos.y, starty) + 'px'; - ev(e); - - var width = Math.abs(pos.x - startx); - var height = Math.abs(pos.y - starty); - var left = Math.min(pos.x, startx); - var top = Math.min(pos.y, starty); - - selbox.style.width = width + 'px'; - selbox.style.height = height + 'px'; - selbox.style.left = left + 'px'; - selbox.style.top = top + 'px'; - - if (IE && window.getSelection) - window.getSelection().removeAllRanges(); - } - - function sel_end(e) { - clearTimeout(ttimer); - ttimer = null; - - if (!is_drag) return; - - if (selbox) { - var sbrect = selbox.getBoundingClientRect(); - var faf = QSA('#ggrid a'); - - for (var a = 0, aa = faf.length; a < aa; a++) - if (bob(sbrect, faf[a].getBoundingClientRect())) - sel_toggle(faf[a]); + if (IE && window.getSelection) + window.getSelection().removeAllRanges(); + } + function sel_end(e) { + clearTimeout(ttimer); + if (dragging && selbox) { + var sbrect = selbox.getBoundingClientRect(); + var faf = QSA('#ggrid a'); + var sadmode = e.shiftKey ? true : e.altKey ? false : "t"; + for (var a = 0, aa = faf.length; a < aa; a++) + if (bob(sbrect, faf[a].getBoundingClientRect())) + sel_toggle(faf[a], sadmode); msel.selui(); - } + ev(e); + } + unbox(); + } - unbox(); - document.body.style.userSelect = 'auto'; - } - - function dsel_init() { + function dsel_init() { window.addEventListener('mousedown', sel_start); window.addEventListener('mousemove', sel_move); window.addEventListener('mouseup', sel_end); - window.addEventListener('touchstart', sel_start, { passive: true }); - window.addEventListener('touchmove', sel_move, { passive: false }); - window.addEventListener('touchend', sel_end, { passive: true }); - - window.addEventListener('dragstart', function(e) { - if (treectl.dsel && (is_selma || is_drag)) { - e.preventDefault(); - } - }); - } + window.addEventListener('touchstart', sel_start, { passive: true }); + window.addEventListener('touchmove', sel_move, { passive: false }); + window.addEventListener('touchend', sel_end, { passive: true }); + window.addEventListener('dragstart', function(e) { + if (treectl.dsel && (is_selma || dragging)) { + e.preventDefault(); + } + }); + } + dsel_init(); })(); From 4e67b467a99521dec931d829592bd8d552c345c7 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 17 Jan 2026 20:23:31 +0000 Subject: [PATCH 16/54] don't mkdir jumpvol realpath --- copyparty/authsrv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 0002f2c8..99492b9f 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2722,7 +2722,7 @@ class AuthSrv(object): for zs in zs.split(): vol.flags.pop(zs, None) - for vol in vfs.all_nodes.values(): + for vol in vfs.all_vols.values(): if not vol.realpath or vol.flags.get("is_file"): continue ccs = vol.flags["casechk"][:1].lower() From 40fb2630977d0afed9a9c7ea76b73906c738930a Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 17 Jan 2026 20:35:06 +0000 Subject: [PATCH 17/54] rephrase "see serverlog" --- copyparty/__main__.py | 2 +- copyparty/authsrv.py | 2 +- copyparty/httpcli.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index d45f165c..1cf04745 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1591,7 +1591,7 @@ def add_safety(ap): ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)") ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") ap2.add_argument("--dont-ban", metavar="TXT", type=u, default="no", help="anyone at this accesslevel or above will not get banned: [\033[32mav\033[0m]=admin-in-volume, [\033[32maa\033[0m]=has-admin-anywhere, [\033[32mrw\033[0m]=read-write, [\033[32mauth\033[0m]=authenticated, [\033[32many\033[0m]=disable-all-bans, [\033[32mno\033[0m]=anyone-can-get-banned") - ap2.add_argument("--banmsg", metavar="TXT", type=u, default="thank you for playing \u00a0 (see serverlog and readme)", help="the response to send to banned users; can be @ban.html to send the contents of ban.html") + ap2.add_argument("--banmsg", metavar="TXT", type=u, default="thank you for playing \u00a0 (see fileserver log and readme)", help="the response to send to banned users; can be @ban.html to send the contents of ban.html") ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 99492b9f..7088682f 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -101,7 +101,7 @@ UP_MTE_MAP = { # db-order } SEE_LOG = "see log for details" -SEESLOG = " (see serverlog for details)" +SEESLOG = " (see fileserver log for details)" SSEELOG = " ({})".format(SEE_LOG) BAD_CFG = "invalid config; {}".format(SEE_LOG) SBADCFG = " ({})".format(BAD_CFG) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index b7e74c6b..01a073f8 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -855,7 +855,7 @@ class HttpCli(object): guess = "modifying" if (origin and host) else "stripping" t = "cors-reject %s because request-header Origin=%r does not match request-protocol %r and host %r based on request-header Host=%r (note: if this request is not malicious, check if your reverse-proxy is accidentally %s request headers, in particular 'Origin', for example by running copyparty with --ihead='*' to show all request headers)" self.log(t % (self.mode, origin, proto, self.host, host, guess), 3) - raise Pebkac(403, "rejected by cors-check (see serverlog)") + raise Pebkac(403, "rejected by cors-check (see fileserver log)") # getattr(self.mode) is not yet faster than this if self.mode == "POST": @@ -3230,7 +3230,7 @@ class HttpCli(object): if num_left < 0: if bail1: return False - raise Pebkac(500, "unconfirmed; see serverlog") + raise Pebkac(500, "unconfirmed; see fileserver log") if not num_left and fpool: with self.u2mutex: From ffb2560322847dfccdfb489e15dfb7a4bcbe25b6 Mon Sep 17 00:00:00 2001 From: stackxp <170874486+stackxp@users.noreply.github.com> Date: Sun, 18 Jan 2026 00:24:53 +0100 Subject: [PATCH 18/54] rcm: add share options (#1216) --- copyparty/web/browser.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 25961847..4cf0d581 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -666,6 +666,9 @@ if (1) "rc_nfi": "new file", "rc_sal": "select all", "rc_sin": "invert selection", + "rc_rnm": "rename", + "rc_shf": "share this folder", + "rc_shs": "share selection", "lang_set": "refresh to make the change take effect?", }; @@ -1051,7 +1054,8 @@ ebi('rcm').innerHTML = ( : '') + '
' + '
' + L.rc_sal + '' + - '' + L.rc_sin + '' + '' + L.rc_sin + '' + + '' ); (function () { @@ -9590,6 +9594,7 @@ var rcm = (function () { selFile.no_dsel = true; break; case 'sin': msel.evsel(null, 't'); break; + case 'shr': fileman.share(); break; } r.hide(true); }; @@ -9627,7 +9632,6 @@ var rcm = (function () { } } } - console.log(selFile); msel.selui(); var has_sel = msel.getsel().length; @@ -9650,6 +9654,9 @@ var rcm = (function () { clmod(ebi('rrnm'), 'hide', !has_sel); clmod(ebi('rs3'), 'hide', !has_sel); clmod(ebi('rs4'), 'hide', !has_sel && !has(perms, "write")); + var shr = ebi('rshr'); + clmod(shr, 'hide', !can_shr || !get_evpath().indexOf(have_shr)); + shr.innerHTML = has_sel ? L.rc_shs : L.rc_shf; menu.style.left = x + 5 + 'px'; menu.style.top = y + 5 + 'px'; From 41d3bae929423d56ac50aaa94066e7c5ffb70ad4 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 17 Jan 2026 23:58:57 +0000 Subject: [PATCH 19/54] override domain for shares (closes #1211); `shr-site` will override the scheme and domain (https://example.com/) in the link to a newly created share, making it possible to create a share from a LAN IP while obtaining an external URL in return --------- Co-authored-by: mechabubba --- copyparty/__main__.py | 2 ++ copyparty/authsrv.py | 5 +++++ copyparty/httpcli.py | 38 +++++++++++++++++++++++++++++--------- copyparty/svchub.py | 4 ++++ copyparty/web/shares.html | 2 +- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 1cf04745..efc8ecb4 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1173,6 +1173,7 @@ def add_general(ap, nc, srvname): ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)") ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)") ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS) + ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") @@ -1220,6 +1221,7 @@ def add_share(ap): ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)") ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share") ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week") + ap2.add_argument("--shr-site", metavar="URL", type=u, default="--site", help="public URL to assume when creating share-links; example: [\033[32mhttps://example.com/\033[0m]") ap2.add_argument("--shr-v", action="store_true", help="debug") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 7088682f..3975f675 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3181,6 +3181,11 @@ class AuthSrv(object): for zs in zs.split(): if getattr(self.args, zs, False): js_htm[zs] = 1 + zs = "up_site" + for zs in zs.split(): + zs2 = getattr(self.args, zs, "") + if zs2: + js_htm[zs] = zs2 vn.js_htm = json_hesc(json.dumps(js_htm)) vols = list(vfs.all_nodes.values()) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 01a073f8..b5098830 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6170,8 +6170,20 @@ class HttpCli(object): zsl = [html_escape(zst[0]) for zst in zstl] r[4] = "
".join(zsl) + if self.args.shr_site: + site = self.args.shr_site[:-1] + elif self.is_vproxied: + site = self.args.SR + else: + site = "" + html = self.j2s( - "shares", this=self, shr=self.args.shr, rows=rows, now=int(time.time()) + "shares", + this=self, + shr=self.args.shr, + site=site, + rows=rows, + now=int(time.time()), ) self.reply(html.encode("utf-8"), status=200) return True @@ -6326,14 +6338,22 @@ class HttpCli(object): fn = quotep(fns[0]) if len(fns) == 1 else "" # NOTE: several clients (frontend, party-up) expect url at response[15:] - surl = "created share: %s://%s%s%s%s/%s" % ( - "https" if self.is_https else "http", - self.host, - self.args.SR, - self.args.shr, - skey, - fn, - ) + if self.args.shr_site: + surl = "created share: %s%s%s/%s" % ( + self.args.shr_site, + self.args.shr[1:], + skey, + fn, + ) + else: + surl = "created share: %s://%s%s%s%s/%s" % ( + "https" if self.is_https else "http", + self.host, + self.args.SR, + self.args.shr, + skey, + fn, + ) self.loud_reply(surl, status=201) return True diff --git a/copyparty/svchub.py b/copyparty/svchub.py index a66848b1..d919d9f2 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -315,6 +315,10 @@ class SvcHub(object): args.doctitle = args.doctitle.replace("--name", args.vname) args.bname = args.bname.replace("--name", args.vname) or args.vname + for zs in ("shr_site",): + if getattr(args, zs) == "--site": + setattr(args, zs, args.site) + if args.log_fk: args.log_fk = re.compile(args.log_fk) diff --git a/copyparty/web/shares.html b/copyparty/web/shares.html index c7da235e..551a3281 100644 --- a/copyparty/web/shares.html +++ b/copyparty/web/shares.html @@ -40,7 +40,7 @@ {%- for k, pw, vp, pr, st, un, t0, t1 in rows %} - qr + qr {{ k }} delete From d9255538100f5196a7e4ffdd78661f68d77cdb4f Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 00:30:46 +0000 Subject: [PATCH 20/54] override domain for uploads (closes #255); `up-site` will override the scheme and domain (https://example.com/) in the links to newly uploaded files, making it possible to upload a file from a LAN IP while obtaining an external URL in return --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 47 ++++++++++++++++++++++++++-------------- copyparty/svchub.py | 2 +- copyparty/web/browser.js | 2 +- copyparty/web/up2k.js | 10 ++++----- copyparty/web/util.js | 4 ++-- tests/util.py | 2 +- 7 files changed, 42 insertions(+), 26 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index efc8ecb4..8d6fdbce 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1229,6 +1229,7 @@ def add_upload(ap): ap2 = ap.add_argument_group("upload options") ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m") ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip") + ap2.add_argument("--up-site", metavar="URL", type=u, default="--site", help="public URL to assume when creating links to uploaded files; example: [\033[32mhttps://example.com/\033[0m]") ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)") ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)") 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)") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index b5098830..9d8092c2 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -2684,11 +2684,20 @@ class HttpCli(object): vpath = "/".join([x for x in [vfs.vpath, rem, fn] if x]) vpath = quotep(vpath) - url = "{}://{}/{}".format( - "https" if self.is_https else "http", - self.host, - self.args.RS + vpath + vsuf, - ) + if self.args.up_site: + url = "%s%s%s" % ( + self.args.up_site, + vpath, + vsuf, + ) + else: + url = "%s://%s/%s%s%s" % ( + "https" if self.is_https else "http", + self.host, + self.args.RS, + vpath, + vsuf, + ) return post_sz, halg, sha_hex, sha_b64, remains, path, url @@ -2962,7 +2971,7 @@ class HttpCli(object): raise Pebkac(500, t % zt) ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :] - if self.is_vproxied: + if self.is_vproxied and not self.args.up_site: if "purl" in ret: ret["purl"] = self.args.SR + ret["purl"] @@ -3840,9 +3849,9 @@ class HttpCli(object): errmsg = "ERROR: " + errmsg if halg: - file_fmt = '{0}: {1} // {2} // {3} bytes // {5} {6}\n' + file_fmt = '{0}: {1} // {2} // {3} bytes // {5} {6}\n' else: - file_fmt = '{3} bytes // {5} {6}\n' + file_fmt = '{3} bytes // {5} {6}\n' for sz, sha_hex, sha_b64, ofn, lfn, ap in files: vsuf = "" @@ -3860,25 +3869,31 @@ class HttpCli(object): if "media" in self.uparam or "medialinks" in vfs.flags: vsuf += "&v" if vsuf else "?v" - vpath = "{}/{}".format(upload_vpath, lfn).strip("/") - rel_url = quotep(self.args.RS + vpath) + vsuf + vpath = vjoin(upload_vpath, lfn) + if self.args.up_site: + ah_url = j_url = self.args.up_site + quotep(vpath) + vsuf + rel_url = "/" + j_url.split("//", 1)[-1].split("/", 1)[-1] + else: + ah_url = rel_url = "/%s%s%s" % (self.args.RS, quotep(vpath), vsuf) + j_url = "%s://%s%s" % ( + "https" if self.is_https else "http", + self.host, + rel_url, + ) + msg += file_fmt.format( halg, sha_hex[:56], sha_b64, sz, - rel_url, + ah_url, html_escape(ofn, crlf=True), vsuf, ) # truncated SHA-512 prevents length extension attacks; # using SHA-512/224, optionally SHA-512/256 = :64 jpart = { - "url": "{}://{}/{}".format( - "https" if self.is_https else "http", - self.host, - rel_url, - ), + "url": j_url, "sz": sz, "fn": lfn, "fn_orig": ofn, diff --git a/copyparty/svchub.py b/copyparty/svchub.py index d919d9f2..98d907bc 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -315,7 +315,7 @@ class SvcHub(object): args.doctitle = args.doctitle.replace("--name", args.vname) args.bname = args.bname.replace("--name", args.vname) or args.vname - for zs in ("shr_site",): + for zs in "shr_site up_site".split(): if getattr(args, zs) == "--site": setattr(args, zs, args.site) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 4cf0d581..fd206caa 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -6525,7 +6525,7 @@ var search_ui = (function () { if (ext.length > 8) ext = '%'; - var links = linksplit(r.rp + '', id).join('/'), + var links = linksplit(r.rp + '', null, id).join('/'), nodes = ['-
' + links + '
' + hsz]; diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index 1a9f3d67..bee1602a 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -1535,7 +1535,7 @@ function up2k_init(subtle) { pvis.addfile([ uc.fsearch ? esc(entry.name) : linksplit( - entry.purl + uricom_enc(entry.name)).join(' / '), + entry.purl + uricom_enc(entry.name), window.up_site).join(' / '), '📐 ' + L.u_hashing, '' ], entry.size, draw_each); @@ -1575,8 +1575,8 @@ function up2k_init(subtle) { more_one_file(); function linklist() { - var ret = [], - base = location.origin.replace(/\/$/, ''); + var ret = [], + base = (window.up_site || location.origin).replace(/\/$/, ''); for (var a = 0; a < st.files.length; a++) { var t = st.files[a], @@ -2562,7 +2562,7 @@ function up2k_init(subtle) { cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b', sdiff = 'diff ' + diff; - msg.push(linksplit(hit.rp).join(' / ') + '
' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '
'); + msg.push(linksplit(hit.rp, window.up_site).join(' / ') + '
' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + ''); } msg = msg.join('
\n'); } @@ -2596,7 +2596,7 @@ function up2k_init(subtle) { url += '?k=' + fk; } - pvis.seth(t.n, 0, linksplit(url).join(' / ')); + pvis.seth(t.n, 0, linksplit(url, window.up_site).join(' / ')); } var chunksize = get_chunksize(t.size), diff --git a/copyparty/web/util.js b/copyparty/web/util.js index 226d2358..ec896d74 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -766,9 +766,9 @@ function assert_vp(path) { } -function linksplit(rp, id) { +function linksplit(rp, base, id) { var ret = [], - apath = '/', + apath = base || '/', q = null; if (rp && rp.indexOf('?') + 1) { diff --git a/tests/util.py b/tests/util.py index bc66512c..f3ccca87 100644 --- a/tests/util.py +++ b/tests/util.py @@ -164,7 +164,7 @@ class Cfg(Namespace): ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ka.update(**{k: 0 for k in ex.split()}) - ex = "ah_alg bname chdir chmod_f chpw_db db_xattr 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" + ex = "ah_alg bname chdir chmod_f chpw_db db_xattr 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 shr_site site tcolor textfiles txt_eol ufavico ufavico_h unlist up_site vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rss_fmt_d rss_fmt_t spinner" From d90d021969499819f935149d0e12cfa9e8692e01 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 20:27:09 +0000 Subject: [PATCH 21/54] readme: windows pathlen warning --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e20b2aba..f38d54db 100644 --- a/README.md +++ b/README.md @@ -399,6 +399,8 @@ same order here too * [Firefox issue 1790500](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500) -- entire browser can crash after uploading ~4000 small files +* Windows: Uploading from a webbrowser may fail with "directory iterator got stuck" due to the [max path length](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry); try moving the files somewhere shorter before uploading + * Android: music playback randomly stops due to [battery usage settings](#fix-unreliable-playback-on-android) * iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11) From 826e84c8ec03c036bdbd31cddaccba992159edac Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 21:03:43 +0000 Subject: [PATCH 22/54] add --flo (logfile format) --- copyparty/__main__.py | 1 + copyparty/svchub.py | 45 ++++++++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 8d6fdbce..fe2e4a2f 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1637,6 +1637,7 @@ def add_logging(ap): ap2 = ap.add_argument_group("logging options") ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages") ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") + ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 98d907bc..3fd30790 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -124,6 +124,7 @@ class SvcHub(object): self.argv = argv self.E: EnvParams = args.E self.no_ansi = args.no_ansi + self.flo = args.flo self.tz = UTC if args.log_utc else None self.logf: Optional[typing.TextIO] = None self.logf_base_fn = "" @@ -1571,21 +1572,38 @@ class SvcHub(object): dt.microsecond // self.log_div, ) - if c and not self.args.no_ansi: - if isinstance(c, int): + if self.flo == 1: + fmt = "@%s [%-21s] %s\n" + if not c: + if "\033" in msg: + msg += "\033[0m" + elif isinstance(c, int): msg = "\033[3%sm%s\033[0m" % (c, msg) elif "\033" not in c: msg = "\033[%sm%s\033[0m" % (c, msg) else: msg = "%s%s\033[0m" % (c, msg) - if "\033" in src: - src += "\033[0m" + if "\033" in src: + src += "\033[0m" + else: + if not c: + fmt = "@%s LOG [%-21s] %s\n" + elif c == 1: + fmt = "@%s CRIT [%-21s] %s\n" + elif c == 3: + fmt = "@%s WARN [%-21s] %s\n" + elif c == 6: + fmt = "@%s BTW [%-21s] %s\n" + else: + fmt = "@%s LOG [%-21s] %s\n" - if "\033" in msg: - msg += "\033[0m" + if "\033" in src: + src = RE_ANSI.sub("", src) + if "\033" in msg: + msg = RE_ANSI.sub("", msg) - self.logf.write("@%s [%-21s] %s\n" % (ts, src, msg)) + self.logf.write(fmt % (ts, src, msg)) if not self.args.no_logflush: self.logf.flush() @@ -1615,9 +1633,10 @@ class SvcHub(object): if self.logf: self.logf.write(zs) - fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" if self.no_ansi: - if c == 1: + if not c: + fmt = "%s %-21s LOG: %s\n" + elif c == 1: fmt = "%s %-21s CRIT: %s\n" elif c == 3: fmt = "%s %-21s WARN: %s\n" @@ -1625,12 +1644,16 @@ class SvcHub(object): fmt = "%s %-21s BTW: %s\n" else: fmt = "%s %-21s LOG: %s\n" + if "\033" in msg: msg = RE_ANSI.sub("", msg) if "\033" in src: src = RE_ANSI.sub("", src) - elif c: - if isinstance(c, int): + else: + fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" + if not c: + pass + elif isinstance(c, int): msg = "\033[3%sm%s\033[0m" % (c, msg) elif "\033" not in c: msg = "\033[%sm%s\033[0m" % (c, msg) From 60ceea4b425858e52aa27c0a7ff91b16bfc3d1bf Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 22:15:34 +0000 Subject: [PATCH 23/54] add nospawn, assert_root --- copyparty/__main__.py | 2 ++ copyparty/authsrv.py | 35 +++++++++++++++++++++++++++++++++++ copyparty/cfg.py | 4 ++++ tests/util.py | 2 +- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index fe2e4a2f..4db2fcc3 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1585,6 +1585,8 @@ def add_safety(ap): ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]") ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)") ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)") + ap2.add_argument("--vol-nospawn", action="store_true", help="if a volume's folder does not exist on the HDD, then do not create it (continue with warning) (volflag=nospawn)") + ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)") ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles") ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile") ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 3975f675..e858cac5 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2090,6 +2090,41 @@ class AuthSrv(object): t = "WARNING: the account [%s] is not mentioned in any volume definitions and thus has the same access-level and privileges that guests have; please see --help-accounts for details. For example, if you intended to give that user full access to the current directory, you could do this: -v .::A,%s" self.log(t % (usr, usr), 1) + dropvols = [] + errors = False + for vol in vfs.all_vols.values(): + if ( + not vol.realpath + or ( + "assert_root" not in vol.flags + and "nospawn" not in vol.flags + and not self.args.vol_or_crash + and not self.args.vol_nospawn + ) + or bos.path.exists(vol.realpath) + ): + pass + elif "assert_root" in vol.flags or self.args.vol_or_crash: + t = "ERROR: volume [/%s] root folder %r does not exist on server HDD; will now crash due to volflag 'assert_root'" + self.log(t % (vol.vpath, vol.realpath), 1) + errors = True + else: + t = "WARNING: volume [/%s] root folder %r does not exist on server HDD; volume will be unavailable due to volflag 'nospawn'" + self.log(t % (vol.vpath, vol.realpath), 3) + dropvols.append(vol) + if errors: + sys.exit(1) + for vol in dropvols: + vol.realpath = "" + vol.axs = AXS() + vol.uaxs = {} + vfs.all_vols.pop(vol.vpath, None) + vfs.all_nodes.pop(vol.vpath, None) + for zv in vfs.all_nodes.values(): + zs = next((x for x, y in zv.nodes.items() if y == vol), "") + if zs: + zv.nodes.pop(zs) + promote = [] demote = [] for vol in vfs.all_vols.values(): diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 163fd4ad..29c05990 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -27,6 +27,8 @@ def vf_bmap() -> dict[str, str]: "no_thumb": "dthumb", "no_vthumb": "dvthumb", "no_athumb": "dathumb", + "vol_nospawn": "nospawn", + "vol_or_crash": "assert_root", } for k in ( "dedup", @@ -427,6 +429,8 @@ flagcats = { "cachectl=no-cache": "controls caching in webbrowsers", "mv_retry": "ms-windows: timeout for renaming busy files", "rm_retry": "ms-windows: timeout for deleting busy files", + "nospawn": "don't create volume's folder if not exist", + "assert_root": "crash on startup if volume's folder not exist", "davauth": "ask webdav clients to login for all folders", "davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)", }, diff --git a/tests/util.py b/tests/util.py index f3ccca87..3afba6bc 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 dlni 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 ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi 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 dlni 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 ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash 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" From 22b9c283d1556c5c53409d51dfd68db3c2a3971d Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 22:22:40 +0000 Subject: [PATCH 24/54] dsel: boomer-proofing s/boomer/ed/ --- copyparty/web/browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index fd206caa..bf6b0a0a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -9775,7 +9775,7 @@ function reload_browser() { var ttimer = null; var lpdelay = 250; - var mvthresh = 10; + var mvthresh = 44; function unbox() { qsr('.selbox'); From c17c3be0086d959e25a93047cee10fd901370065 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 23:10:11 +0000 Subject: [PATCH 25/54] wo_up_readme according to volflags; now that the filenames of logues/readmes can be customized, match against the configured names rather than the defaults --- copyparty/authsrv.py | 7 +++++++ copyparty/ftpd.py | 3 +-- copyparty/httpcli.py | 3 +-- copyparty/sftpd.py | 3 +-- copyparty/tftpd.py | 3 +-- copyparty/up2k.py | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index e858cac5..f248e0df 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1070,6 +1070,7 @@ class AuthSrv(object): "tcolor": self.args.tcolor, "du_iwho": self.args.du_iwho, "shr_who": self.args.shr_who if self.args.shr else "no", + "emb_all": FN_EMB, "ls_q_m": ("", ""), } self._vf0 = self._vf0b.copy() @@ -2573,16 +2574,22 @@ class AuthSrv(object): t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s" self.log(t % (vol.vpath, etv), 3) + emb_all = vol.flags["emb_all"] = set() + zsl1 = [x for x in vol.flags["preadmes"].split(",") if x] zsl2 = [x for x in vol.flags["readmes"].split(",") if x] zsl3 = list(set([x.lower() for x in zsl1])) zsl4 = list(set([x.lower() for x in zsl2])) + emb_all.update(zsl3) + emb_all.update(zsl4) vol.flags["emb_mds"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]] zsl1 = [x for x in vol.flags["prologues"].split(",") if x] zsl2 = [x for x in vol.flags["epilogues"].split(",") if x] zsl3 = list(set([x.lower() for x in zsl1])) zsl4 = list(set([x.lower() for x in zsl2])) + emb_all.update(zsl3) + emb_all.update(zsl4) vol.flags["emb_lgs"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]] zs = str(vol.flags.get("html_head") or "") diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index c1bc543b..e5d8ff9e 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -19,7 +19,6 @@ from .__init__ import PY2, TYPE_CHECKING from .authsrv import VFS from .bos import bos from .util import ( - FN_EMB, VF_CAREFUL, Daemon, ODict, @@ -179,7 +178,7 @@ class FtpFs(AbstractedFS): vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) if ( w - and fn.lower() in FN_EMB + and fn.lower() in vfs.flags["emb_all"] and self.h.uname not in vfs.axs.uread and "wo_up_readme" not in vfs.flags ): diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 9d8092c2..785f85ac 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -45,7 +45,6 @@ from .util import ( BITNESS, DAV_ALLPROPS, E_SCK_WR, - FN_EMB, HAVE_SQLITE3, HTTPCODE, UTC, @@ -2922,7 +2921,7 @@ class HttpCli(object): if ( not self.can_read and self.can_write - and name.lower() in FN_EMB + and name.lower() in dbv.flags["emb_all"] and "wo_up_readme" not in dbv.flags ): name = "_wo_" + name diff --git a/copyparty/sftpd.py b/copyparty/sftpd.py index deb92042..790ff5b6 100644 --- a/copyparty/sftpd.py +++ b/copyparty/sftpd.py @@ -26,7 +26,6 @@ from .__init__ import ANYWIN, TYPE_CHECKING from .authsrv import LEELOO_DALLAS, VFS, AuthSrv from .bos import bos from .util import ( - FN_EMB, VF_CAREFUL, Daemon, ODict, @@ -328,7 +327,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface): vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) if ( w - and fn.lower() in FN_EMB + and fn.lower() in vn.flags["emb_all"] and self.uname not in vn.axs.uread and "wo_up_readme" not in vn.flags ): diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 9eb76643..8e4c70b7 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -37,7 +37,6 @@ from .__init__ import EXE, PY2, TYPE_CHECKING from .authsrv import VFS from .bos import bos from .util import ( - FN_EMB, UTC, BytesIO, Daemon, @@ -268,7 +267,7 @@ class Tftpd(object): vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) if perms[1] and "*" not in vfs.axs.uread and "wo_up_readme" not in vfs.flags: zs, fn = vsplit(vpath) - if fn.lower() in FN_EMB: + if fn.lower() in vfs.flags["emb_all"]: vpath = vjoin(zs, "_wo_" + fn) vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 81c221f1..b5356bfd 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -1152,7 +1152,7 @@ class Up2k(object): ft = "\033[0;32m{}{:.0}" ff = "\033[0;35m{}{:.0}" fv = "\033[0;36m{}:\033[90m{}" - zs = "bcasechk du_iwho emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v" + zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v" fx = set(zs.split()) fd = vf_bmap() fd.update(vf_cmap()) From 67c5d8dae4cfeddd6882aaf63c61bbba4f9bcff6 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 23:17:37 +0000 Subject: [PATCH 26/54] u2c: server-offline timeout; closes #1197 --- bin/u2c.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/bin/u2c.py b/bin/u2c.py index dd97784c..25877a96 100755 --- a/bin/u2c.py +++ b/bin/u2c.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 from __future__ import print_function, unicode_literals -S_VERSION = "2.18" -S_BUILD_DT = "2026-01-02" +S_VERSION = "2.19" +S_BUILD_DT = "2026-01-18" """ u2c.py: upload to copyparty @@ -100,7 +100,7 @@ except: ub64enc = base64.urlsafe_b64encode -class BadAuth(Exception): +class Fatal(Exception): pass @@ -835,10 +835,15 @@ def handshake(ar, file, search): url = "" url = ar.vtop + url + t0 = time.time() + tmax = t0 + ar.t_hs while True: sc = 600 txt = "" - t0 = time.time() + t1 = time.time() + if t1 >= tmax: + print("\nERROR: server offline for longer than --t-hs; giving up") + raise Fatal() try: zs = json.dumps(req, separators=(",\n", ": ")) sc, txt = web.req("POST", url, {}, zs.encode("utf-8"), MJ) @@ -861,11 +866,11 @@ def handshake(ar, file, search): return [], False elif sc == 403 or sc == 401: print("\nERROR: login required, or wrong password:\n%s" % (txt,)) - raise BadAuth() + raise Fatal() - t = "handshake failed, retrying: %s\n t0=%.3f t1=%.3f td=%.3f\n %s\n\n" + t = "handshake failed, retrying: %s\n t0=%.3f t1=%.3f t2=%.3f td1=%.3f td2=%.3f\n %s\n\n" now = time.time() - eprint(t % (file.name, t0, now, now - t0, em)) + eprint(t % (file.name, t0, t1, now, now - t0, now - t1, em)) time.sleep(ar.cd) try: @@ -1051,7 +1056,7 @@ class Ctl(object): print(" hs...") try: hs, _ = handshake(self.ar, file, search) - except BadAuth: + except Fatal: sys.exit(1) if search: @@ -1356,7 +1361,7 @@ class Ctl(object): try: hs, sprs = handshake(self.ar, file, search) - except BadAuth: + except Fatal: self.panik = 1 break @@ -1591,6 +1596,7 @@ NOTE: if server has --usernames enabled, then password is "username:password" ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)") ap.add_argument("--cxp", type=float, metavar="SEC", default=57, help="assume http connections expired after SEConds") ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload") + ap.add_argument("--t-hs", type=float, metavar="SEC", default=186, help="crash if handshakes fail due to server-offline for this long") ap.add_argument("--safe", action="store_true", help="use simple fallback approach") ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)") From df8b395380336aa9e7cc7fe4b26d30af31f49478 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Jan 2026 23:22:13 +0000 Subject: [PATCH 27/54] =?UTF-8?q?=EF=BC=88=E3=80=80=C2=B4=5F=E3=82=9D`?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- copyparty/authsrv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index f248e0df..0bd70958 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -22,6 +22,7 @@ from .util import ( DEF_MTH, EXTS, FAVICON_MIMES, + FN_EMB, HAVE_SQLITE3, IMPLICATIONS, META_NOBOTS, From 206b1752e61bbaf9fd3cb120af0e1aea6144d331 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 19 Jan 2026 00:09:55 +0000 Subject: [PATCH 28/54] mtl new strings --- copyparty/web/browser.js | 1 - copyparty/web/tl/chi.js | 5 +++++ copyparty/web/tl/cze.js | 5 +++++ copyparty/web/tl/deu.js | 5 +++++ copyparty/web/tl/epo.js | 5 +++++ copyparty/web/tl/fin.js | 4 ++++ copyparty/web/tl/fra.js | 5 +++++ copyparty/web/tl/grc.js | 5 +++++ copyparty/web/tl/ita.js | 5 +++++ copyparty/web/tl/jpn.js | 5 +++++ copyparty/web/tl/kor.js | 5 +++++ copyparty/web/tl/nld.js | 5 +++++ copyparty/web/tl/nno.js | 5 +++++ copyparty/web/tl/nor.js | 4 ++++ copyparty/web/tl/pol.js | 5 +++++ copyparty/web/tl/por.js | 5 +++++ copyparty/web/tl/rus.js | 5 +++++ copyparty/web/tl/spa.js | 5 +++++ copyparty/web/tl/swe.js | 5 +++++ copyparty/web/tl/tur.js | 5 +++++ copyparty/web/tl/ukr.js | 5 +++++ copyparty/web/tl/vie.js | 5 +++++ scripts/tl.js | 2 ++ 23 files changed, 105 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index bf6b0a0a..372f72e9 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -666,7 +666,6 @@ if (1) "rc_nfi": "new file", "rc_sal": "select all", "rc_sin": "invert selection", - "rc_rnm": "rename", "rc_shf": "share this folder", "rc_shs": "share selection", diff --git a/copyparty/web/tl/chi.js b/copyparty/web/tl/chi.js index e335ed06..3992ddef 100644 --- a/copyparty/web/tl/chi.js +++ b/copyparty/web/tl/chi.js @@ -226,6 +226,7 @@ Ls.chi = { "ct_ttips": '◔ ◡ ◔">ℹ️ 工具提示', "ct_thumb": '在网格视图中,切换图标或缩略图$N快捷键: T">🖼️ 缩略图', "ct_csel": '在网格视图中使用 CTRL 和 SHIFT 进行文件选择">CTRL', + "ct_dsel": '在网格视图中使用拖动选择">拖动', //m "ct_dl": '点击文件时强制下载(不内联显示)">dl', //m "ct_ihop": '当图像查看器关闭时,滚动到最后查看的文件">滚动', "ct_dots": '显示隐藏文件(如果服务器允许)">隐藏文件', @@ -264,6 +265,7 @@ Ls.chi = { "cdt_ask": "滚动到底部时,$N不会加载更多文件,$N而是询问你该怎么做", "cdt_hsort": "包含在媒体 URL 中的排序规则 (<code>,sorthref</code>) 数量。将其设置为 0 时,点击媒体链接时也会忽略排序规则。", //m "cdt_ren": "启用自定义右键菜单,按住 shift 键并右键单击仍可访问常规菜单", //m + "cdt_rdb": "当自定义右键菜单已打开并再次右键点击时显示常规右键菜单", //m "tt_entree": "显示导航面板(目录树侧边栏)$N快捷键: B", "tt_detree": "显示面包屑导航$N快捷键: B", @@ -654,10 +656,13 @@ Ls.chi = { "rc_cut": "剪切", //m "rc_cpy": "复制", //m "rc_pst": "粘贴", //m + "rc_rnm": "重命名", //m "rc_nfo": "新建文件夹", //m "rc_nfi": "新建文件", //m "rc_sal": "全选", //m "rc_sin": "反选", //m + "rc_shf": "共享此文件夹", //m + "rc_shs": "共享所选内容", //m "lang_set": "刷新以使更改生效?", diff --git a/copyparty/web/tl/cze.js b/copyparty/web/tl/cze.js index e5b43047..a1c8c088 100644 --- a/copyparty/web/tl/cze.js +++ b/copyparty/web/tl/cze.js @@ -230,6 +230,7 @@ Ls.cze = { "ct_ttips": '◔ ◡ ◔">ℹ️ nápovědy', "ct_thumb": 'v zobrazení mřížky přepnout ikony nebo náhledy$NKlávesová zkratka: T">🖼️ náhledy', "ct_csel": 'použít CTRL a SHIFT pro výběr souborů v zobrazení mřížky">výběr', + "ct_dsel": 'použít tažený výběr v zobrazení mřížky">tažení', //m "ct_dl": 'vynutit stažení (nezobrazovat inline) při kliknutí na soubor">dl', //m "ct_ihop": 'když se zavře prohlížeč obrázků, posunout dolů k naposledy zobrazenému souboru">g⮯', "ct_dots": 'zobrazit skryté soubory (pokud to server povoluje)">dotfiles', @@ -268,6 +269,7 @@ Ls.cze = { "cdt_ask": "při posunování na konec,$Nmísto načítání více souborů,$N se zeptat co dělat", "cdt_hsort": "kolik pravidel řazení (<code>,sorthref</code>) zahrnout do media-URL. Nastavení na 0 bude také ignorovat pravidla řazení zahrnutá v media odkazech při kliknutí na ně", "cdt_ren": "povolit vlastní kontextovou nabídku, běžnou nabídku lze otevřít podržením klávesy shift a kliknutím pravým tlačítkem", //m + "cdt_rdb": "zobrazit běžné menu pravého tlačítka, když je vlastní již otevřené a znovu se klikne pravým", //m "tt_entree": "zobrazit navigační panel (postranní strom adresářů)$NKlávesová zkratka: B", "tt_detree": "zobrazit drobečkovou navigaci$NKlávesová zkratka: B", @@ -658,10 +660,13 @@ Ls.cze = { "rc_cut": "vyjmout", //m "rc_cpy": "kopírovat", //m "rc_pst": "vložit", //m + "rc_rnm": "přejmenovat", //m "rc_nfo": "nová složka", //m "rc_nfi": "nový soubor", //m "rc_sal": "vybrat vše", //m "rc_sin": "invertovat výběr", //m + "rc_shf": "sdílet tuto složku", //m + "rc_shs": "sdílet výběr", //m "lang_set": "obnovit stránku, aby se změna projevila?", diff --git a/copyparty/web/tl/deu.js b/copyparty/web/tl/deu.js index f040a28a..c9e35b3e 100644 --- a/copyparty/web/tl/deu.js +++ b/copyparty/web/tl/deu.js @@ -226,6 +226,7 @@ Ls.deu = { "ct_ttips": '◔ ◡ ◔">ℹ️ Tooltips', "ct_thumb": 'In Raster-Ansicht, zwischen Icons und Vorschau wechseln$NHotkey: T">🖼️ Vorschaubilder', "ct_csel": 'Benutze STRG und UMSCHALT für Dateiauswahl in Raster-Ansicht">sel', + "ct_dsel": 'Ziehauswahl in Raster-Ansicht verwenden">ziehen', //m "ct_dl": 'Beim Klick auf Dateien sie immer herunterladen (nicht einbetten)">dl', "ct_ihop": 'Wenn die Bildanzeige geschlossen ist, scrolle runter zu den zuletzt angesehenen Dateien">g⮯', "ct_dots": 'Verstecke Dateien anzeigen (wenn durch den Server erlaubt)">dotfiles', @@ -264,6 +265,7 @@ Ls.deu = { "cdt_ask": "beim Runterscrollen nach $NAktion fragen statt mehr,$NDateien zu laden", "cdt_hsort": "Menge an Sortierregeln (<code>,sorthref</code>) in Media-URLs enthalten sein sollen. Ein Wert von 0 sorgt dafür, dass Sortierregeln in Media-URLs ignoriert werden", "cdt_ren": "spezielles Rechtsklick-Menü aktivieren, das Browser-Menü ist weiterhin mit Shift + Rechtsklick erreichbar", + "cdt_rdb": "normales Rechtsklick-Menü anzeigen, wenn das benutzerdefinierte bereits offen ist und erneut rechts geklickt wird", //m "tt_entree": "Navpane anzeigen (Ordnerbaum Sidebar)$NHotkey: B", "tt_detree": "Breadcrumbs anzeigen$NHotkey: B", @@ -654,10 +656,13 @@ Ls.deu = { "rc_cut": "ausschneiden", "rc_cpy": "kopieren", "rc_pst": "einfügen", + "rc_rnm": "umbenennen", //m "rc_nfo": "neuer Ordner", "rc_nfi": "neue Datei", "rc_sal": "alles auswählen", "rc_sin": "auswahl umkehren", + "rc_shf": "diesen ordner teilen", //m + "rc_shs": "auswahl teilen", //m "lang_set": "Neuladen um Änderungen anzuwenden?", diff --git a/copyparty/web/tl/epo.js b/copyparty/web/tl/epo.js index c532a34c..af497407 100644 --- a/copyparty/web/tl/epo.js +++ b/copyparty/web/tl/epo.js @@ -226,6 +226,7 @@ Ls.epo = { "ct_ttips": '◔ ◡ ◔">ℹ️ ŝpruchelpiloj', "ct_thumb": 'dum krado-vido, baskuli montradon de simboloj aŭ bildetoj$NFulmoklavo: T">🖼️ bildetoj', "ct_csel": 'uzi STIR kaj MAJ por elekti dosierojn en krado-vido">elekto', + "ct_dsel": 'uzi tren-elekton en krado-vido">treni', //m "ct_dl": 'devigi elŝuton (ne montri enkadre) kiam dosiero estas alklakita">dl', //m "ct_ihop": 'montri la lastan viditan bildo-dosieron post fermado de bildo-vidilo">g⮯', "ct_dots": 'montri kaŝitajn dosierojn (se servilo permesas)">kaŝitaj', @@ -264,6 +265,7 @@ Ls.epo = { "cdt_ask": "je malsupro de paĝo, peti por ago$Nanstataŭ ŝarĝi pli da dosieroj", "cdt_hsort": "kiom da ordigo-reguloj (<code>,sorthref</code>) inkludi en adreso de la paĝo. Se agordita kiel 0, reguloj, inkluditaj en la adreso, estos ignoritaj", "cdt_ren": "ebligi propran dekstra-klakan menuon, la normala menuo restas alirebla per shift + dekstra klako", //m + "cdt_rdb": "montri la normalan dekstraklakan menuon kiam la propra jam estas malfermita kaj oni denove dekstre klakas", //m "tt_entree": "montri arbovidan navig-panelon$NFulmoklavo: B", "tt_detree": "montri paĝnivelan navig-panelon$NFulmoklavo: B", @@ -654,10 +656,13 @@ Ls.epo = { "rc_cut": "eltondi", //m "rc_cpy": "kopii", //m "rc_pst": "alglui", //m + "rc_rnm": "alinomi", //m "rc_nfo": "nova dosierujo", //m "rc_nfi": "nova dosiero", //m "rc_sal": "elekti ĉion", //m "rc_sin": "inversigi elekton", //m + "rc_shf": "kunhavigi ĉi tiun dosierujon", //m + "rc_shs": "kunhavigi elekton", //m "lang_set": "ĉu reŝargi paĝon por efektivigi lingvo-ŝanĝon?", diff --git a/copyparty/web/tl/fin.js b/copyparty/web/tl/fin.js index a680a04f..4ca9ed41 100644 --- a/copyparty/web/tl/fin.js +++ b/copyparty/web/tl/fin.js @@ -265,6 +265,7 @@ Ls.fin = { "cdt_ask": "sivun lopussa, sen sijaan että lataa $Nautomaattisesti lisää tiedostoja, kysy mitä tehdä", "cdt_hsort": "kuinka monta lajittelusääntöä (<code>,sorthref</code>) sisällyttää media-URL:eihin. Tämän asettaminen nollaan jättää myös huomioimatta media-linkeissä sisällytetyt lajittelusäännöt kun napsautat niitä", "cdt_ren": "ota käyttöön mukautettu valikko, tavallinen valikko on käytettävissä painamalla shift ja napsauttamalla oikealla", //m + "cdt_rdb": "näytä tavallinen oikean painikkeen valikko, kun mukautettu on jo auki ja oikeaa painiketta painetaan uudelleen", //m "tt_entree": "näytä navigointipaneeli$NPikanäppäin: B", "tt_detree": "näytä linkkipolku$NPikanäppäin: B", @@ -655,10 +656,13 @@ Ls.fin = { "rc_cut": "Leikkaa", //m "rc_cpy": "kopioi", //m "rc_pst": "Liitä", //m + "rc_rnm": "nimeä uudelleen", //m "rc_nfo": "uusi kansio", //m "rc_nfi": "uusi tiedosto", //m "rc_sal": "valitse kaikki", //m "rc_sin": "käännä valinta", //m + "rc_shf": "jaa tämä kansio", //m + "rc_shs": "jaa valinta", //m "lang_set": "ladataanko sivu uudestaan kielen vaihtamiseksi?", diff --git a/copyparty/web/tl/fra.js b/copyparty/web/tl/fra.js index a8ab1649..aeb5b09a 100644 --- a/copyparty/web/tl/fra.js +++ b/copyparty/web/tl/fra.js @@ -226,6 +226,7 @@ Ls.fra = { "ct_ttips": '◔ ◡ ◔">ℹ️ infobulles', "ct_thumb": 'vue en grille, activer les icônes ou les miniatures$NHotkey: T">🖼️ minia', "ct_csel": 'utiliser CTRL et MAJ pour selectioner des fichiers en vue en grille">sel', + "ct_dsel": 'utiliser la sélection par glisser en vue en grille">glisser', //m "ct_dl": 'forcer le téléchargement (ne pas afficher en ligne) lorsqu’un fichier est cliqué">dl', //m "ct_ihop": 'quand le visionneuse d\'image est fermé, faire defiller vers le bas jusqu\'au dernier fichier">g⮯', "ct_dots": 'voir les fichiers caché (si le serveur le permet)">dotfiles', @@ -264,6 +265,7 @@ Ls.fra = { "cdt_ask": "lorsque vous faites défiler vers le bas,$Nau lieu de charger plus de fichiers,$Ndemander quoi faire", "cdt_hsort": "combien de règles de tri (<code>,sorthref</code>) à inclure dans les media-URLs. Définir cette valeur à 0 ignorera également les règles de tri incluses dans les liens média lorsque vous cliquez dessus.", "cdt_ren": "activer le menu contextuel personnalisé, le menu normal reste accessible avec shift + clic droit", //m + "cdt_rdb": "afficher le menu clic droit normal lorsque le menu personnalisé est déjà ouvert et qu’on clique à nouveau", //m "tt_entree": "afficher le panneau de navigation (arborescence des dossiers)$NHotkey: B", "tt_detree": "afficher le fil d’Ariane$NHotkey: B", @@ -654,10 +656,13 @@ Ls.fra = { "rc_cut": "couper", //m "rc_cpy": "copier", //m "rc_pst": "coller", //m + "rc_rnm": "renommer", //m "rc_nfo": "nouveau dossier", //m "rc_nfi": "nouveau fichier", //m "rc_sal": "tout sélectionner", //m "rc_sin": "inverser la sélection", //m + "rc_shf": "partager ce dossier", //m + "rc_shs": "partager la sélection", //m "lang_set": "rafraîchir pour que les changements prennent effet ?", diff --git a/copyparty/web/tl/grc.js b/copyparty/web/tl/grc.js index 94065e9f..e2308479 100644 --- a/copyparty/web/tl/grc.js +++ b/copyparty/web/tl/grc.js @@ -226,6 +226,7 @@ Ls.grc = { "ct_ttips": '◔ ◡ ◔">ℹ️ συμβουλές εργαλείων', "ct_thumb": 'σε προβολή πλέγματος, εναλλαγή εικονιδίων ή μικρογραφιών$NΠλήκτρο συντόμευσης: T">🖼️ μικρογραφίες', "ct_csel": 'χρησιμοποίησε CTRL και SHIFT για επιλογή αρχείων σε προβολή πλέγματος">επιλογή', + "ct_dsel": 'χρησιμοποίησε επιλογή με σύρσιμο σε προβολή πλέγματος">σύρσιμο', //m "ct_dl": 'εξαναγκασμός λήψης (να μην εμφανίζεται ενσωματωμένα) όταν γίνεται κλικ σε ένα αρχείο">dl', //m "ct_ihop": 'όταν η προβολή εικόνων κλείνει, κάνε scroll στο τελευταίο προβαλλόμενο αρχείο">g⮯', "ct_dots": 'εμφάνιση κρυφών αρχείων (αν το επιτρέπει ο server)">dotfiles', @@ -264,6 +265,7 @@ Ls.grc = { "cdt_ask": "όταν φτάνεις στο τέλος,$Nαντί να φορτώσει περισσότερα αρχεία,$Nρωτά τι να κάνει", "cdt_hsort": "πόσους κανόνες ταξινόμησης (<code>,sorthref</code>) να συμπεριλάβει σε URLs πολυμέσων. Αν το βάλεις 0 αγνοεί και κανόνες ταξινόμησης στους συνδέσμους πολυμέσων", "cdt_ren": "ενεργοποίηση προσαρμοσμένου μενού δεξιού κλικ, το κανονικό μενού είναι προσβάσιμο με shift + δεξί κλικ", //m + "cdt_rdb": "εμφάνιση του κανονικού μενού δεξιού κλικ όταν το προσαρμοσμένο είναι ήδη ανοιχτό και γίνεται ξανά δεξί κλικ", //m "tt_entree": "εμφάνιση navpane (δέντρο διαδρομών)$NΠλήκτρο συντόμευσης: B", "tt_detree": "εμφάνιση breadcrumbs (καρτέλες διαδρομών)$NΠλήκτρο συντόμευσης: B", @@ -654,10 +656,13 @@ Ls.grc = { "rc_cut": "αποκοπή", //m "rc_cpy": "αντιγραφή", //m "rc_pst": "επικόλληση", //m + "rc_rnm": "μετονομασία", //m "rc_nfo": "νέος φάκελος", //m "rc_nfi": "νέο αρχείο", //m "rc_sal": "επιλογή όλων", //m "rc_sin": "αντιστροφή επιλογής", //m + "rc_shf": "κοινή χρήση αυτού του φακέλου", //m + "rc_shs": "κοινή χρήση επιλογής", //m "lang_set": "ανανέωση σελίδας για εφαρμογή της αλλαγής;", diff --git a/copyparty/web/tl/ita.js b/copyparty/web/tl/ita.js index 77a0276b..b4b108b5 100644 --- a/copyparty/web/tl/ita.js +++ b/copyparty/web/tl/ita.js @@ -226,6 +226,7 @@ Ls.ita = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltip', "ct_thumb": 'nella vista griglia, alterna icone o miniature$NTasto rapido: T">🖼️ miniature', "ct_csel": 'usa CTRL e SHIFT per la selezione file nella vista griglia">sel', + "ct_dsel": 'usa la selezione tramite trascinamento nella vista griglia">trascina', //m "ct_dl": 'forza il download (non visualizzare inline) quando si clicca su un file">dl', //m "ct_ihop": 'quando il visualizzatore immagini è chiuso, scorri fino all\'ultimo file visualizzato">g⮯', "ct_dots": 'mostra file nascosti (se il server lo permette)">dotfile', @@ -264,6 +265,7 @@ Ls.ita = { "cdt_ask": "quando scorri verso il fondo,$Ninvece di caricare più file,$Nchiedi cosa fare", "cdt_hsort": "quante regole di ordinamento (<code>,sorthref</code>) includere negli URL multimediali. Impostandolo a 0 ignorerà anche le regole di ordinamento incluse nei link multimediali quando li clicchi", "cdt_ren": "abilita il menu contestuale personalizzato, il menu normale è accessibile con shift + clic destro", //m + "cdt_rdb": "mostra il menu normale con il tasto destro quando quello personalizzato è già aperto e si clicca di nuovo", //m "tt_entree": "mostra pannello nav (barra laterale albero directory)$NTasto rapido: B", "tt_detree": "mostra breadcrumb$NTasto rapido: B", @@ -654,10 +656,13 @@ Ls.ita = { "rc_cut": "taglia", //m "rc_cpy": "copia", //m "rc_pst": "incolla", //m + "rc_rnm": "rinomina", //m "rc_nfo": "nuova cartella", //m "rc_nfi": "nuovo file", //m "rc_sal": "seleziona tutto", //m "rc_sin": "inverti selezione", //m + "rc_shf": "condividi questa cartella", //m + "rc_shs": "condividi selezione", //m "lang_set": "aggiornare per rendere effettivo il cambiamento?", diff --git a/copyparty/web/tl/jpn.js b/copyparty/web/tl/jpn.js index 2f97a7c5..807cd861 100644 --- a/copyparty/web/tl/jpn.js +++ b/copyparty/web/tl/jpn.js @@ -226,6 +226,7 @@ Ls.jpn = { "ct_ttips": '◔ ◡ ◔">ℹ️ ツールチップ', "ct_thumb": 'グリッドビューではアイコンまたはサムネイルを切り替える$Nホットキー: T">🖼️ サムネイル', "ct_csel": 'グリッドビューでファイルを選択するにはCtrlとShiftを使用する。">選択', + "ct_dsel": 'グリッドビューでドラッグ選択を使用する。">ドラッグ', //m "ct_dl": 'ファイルをクリックしたときに強制的にダウンロードする(インラインで表示しない)">dl', "ct_ihop": '画像ビューアを閉じたら最後に表示したファイルまでスクロールする。">g⮯', "ct_dots": '隠しファイルを表示する(サーバーが許可している場合)">隠しファイル', @@ -264,6 +265,7 @@ Ls.jpn = { "cdt_ask": "一番下までスクロールしたときに$N更にファイルを読み込む代わりに$N何をするか尋ねる", "cdt_hsort": "メディアURLに含めるソートルール (<code>,sorthref</code>) の数。0に設定するとメディアリンクをクリックした際にそのリンクに含まれるソートルールも無視されます。", "cdt_ren": "カスタム右クリックメニューを有効にしてもShiftキーを押しながら右クリックすることで通常のメニューにアクセスできます。", + "cdt_rdb": "カスタム右クリックメニューが開いている状態で再度右クリックしたときに通常のメニューを表示する", //m "tt_entree": "ナビペインを表示(ディレクトリツリーサイドバー)$Nホットキー: B", "tt_detree": "パンくずリストを表示$Nホットキー: B", @@ -654,10 +656,13 @@ Ls.jpn = { "rc_cut": "切り取り", "rc_cpy": "コピー", "rc_pst": "貼り付け", + "rc_rnm": "名前を変更", //m "rc_nfo": "新しいフォルダ", "rc_nfi": "新しいファイル", "rc_sal": "すべて選択", "rc_sin": "選択を反転", + "rc_shf": "このフォルダを共有", //m + "rc_shs": "選択項目を共有", //m "lang_set": "変更を反映させるために更新しますか?", diff --git a/copyparty/web/tl/kor.js b/copyparty/web/tl/kor.js index 6bebbd64..3f71eb19 100644 --- a/copyparty/web/tl/kor.js +++ b/copyparty/web/tl/kor.js @@ -226,6 +226,7 @@ Ls.kor = { "ct_ttips": '◔ ◡ ◔">ℹ️ 도움말', "ct_thumb": '그리드 보기에서 아이콘 또는 미리보기 이미지 전환$N단축키: T">🖼️ 미리보기', "ct_csel": '그리드 보기에서 CTRL과 SHIFT를 사용하여 파일 선택">선택', + "ct_dsel": '그리드 보기에서 드래그 선택 사용">드래그', //m "ct_dl": '파일을 클릭하면 다운로드를 강제로 수행 (인라인으로 표시하지 않음)">dl', //m "ct_ihop": '이미지 뷰어를 닫으면 마지막으로 본 파일로 스크롤">g⮯', "ct_dots": '숨김 파일 표시 (서버가 허용하는 경우)">숨김파일', @@ -264,6 +265,7 @@ Ls.kor = { "cdt_ask": "맨 아래로 스크롤할 때$N더 많은 파일을 불러오는 대신$N무엇을 할지 묻기", "cdt_hsort": "미디어 URL에 포함할 정렬 규칙 (<code>,sorthref</code>)의 수. 0으로 설정하면 미디어 링크를 클릭할 때 포함된 정렬 규칙도 무시됩니다.", "cdt_ren": "사용자 지정 우클릭 메뉴를 활성화합니다. shift 키를 누른 채 우클릭하면 기본 메뉴를 사용할 수 있습니다", //m + "cdt_rdb": "사용자 정의 우클릭 메뉴가 이미 열려 있을 때 다시 우클릭하면 기본 메뉴 표시", //m "tt_entree": "탐색 창 (디렉터리 트리 사이드바) 표시$N단축키: B", "tt_detree": "이동 경로 표시$N단축키: B", @@ -654,10 +656,13 @@ Ls.kor = { "rc_cut": "잘라내기", //m "rc_cpy": "복사", //m "rc_pst": "붙여넣기", //m + "rc_rnm": "이름 변경", //m "rc_nfo": "새 폴더", //m "rc_nfi": "새 파일", //m "rc_sal": "모두 선택", //m "rc_sin": "선택 반전", //m + "rc_shf": "이 폴더 공유", //m + "rc_shs": "선택 항목 공유", //m "lang_set": '변경 사항을 적용하기 위해 새로고침하시겠습니까?', diff --git a/copyparty/web/tl/nld.js b/copyparty/web/tl/nld.js index b4287b96..a5b38095 100644 --- a/copyparty/web/tl/nld.js +++ b/copyparty/web/tl/nld.js @@ -226,6 +226,7 @@ Ls.nld = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'In grid-overzicht, wissel tussen iconen of thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'Gebruik CTRL en SHIFT voor de bestand selectie in grid-overzicht>sel', + "ct_dsel": 'Gebruik slepen om te selecteren in grid-overzicht>slepen', //m "ct_dl": 'download afdwingen (niet inline weergeven) wanneer op een bestand wordt geklikt">dl', //m "ct_ihop": 'Als je afbeeldingviewer afsluit, scroll omlaag naar de laatst bekeken bestand">g⮯', "ct_dots": 'Laat verborgen bestanden zien (als de server dat toestaat)">dotfiles', @@ -264,6 +265,7 @@ Ls.nld = { "cdt_ask": "Als helemaal naar beneden gescrolld bent,$Nin plaats van meer inladen,$Nvraag wat het moet doen", "cdt_hsort": "Hoeveel sorteerregels (<code>,sorthref</code>) moeten er in media-URL's worden opgenomen? Als je dit op 0 instelt, worden de sorteerregels in medialinks ook genegeerd wanneer erop geklikt word.", "cdt_ren": "Aangepast rechtermuisknopmenu inschakelen, het normale menu blijft beschikbaar met shift + rechtermuisknop", //m + "cdt_rdb": "toon het normale rechtermuisknopmenu wanneer het aangepaste al open is en opnieuw wordt geklikt", //m "tt_entree": "Laat navpane zien (directoryboom zijbalk)$NHotkey: B", "tt_detree": "Laat breadcrumbs zien$NHotkey: B", @@ -654,10 +656,13 @@ Ls.nld = { "rc_cut": "Knippen", //m "rc_cpy": "Kopiëren", //m "rc_pst": "Plakken", //m + "rc_rnm": "hernoemen", //m "rc_nfo": "Nieuwe map", //m "rc_nfi": "Nieuw bestand", //m "rc_sal": "Alles selecteren", //m "rc_sin": "Selectie omkeren", //m + "rc_shf": "deel deze map", //m + "rc_shs": "deel selectie", //m "lang_set": "Vernieuw de pagina om de wijziging door te voeren?", diff --git a/copyparty/web/tl/nno.js b/copyparty/web/tl/nno.js index 796ee8f5..45b3afd2 100644 --- a/copyparty/web/tl/nno.js +++ b/copyparty/web/tl/nno.js @@ -223,6 +223,7 @@ Ls.nno = { "ct_ttips": 'vis hjelpetekst ved å holde musa over ting">ℹ️ tips', "ct_thumb": 'vis miniatyrbilder i staden for ikon$NSnarvei: T">🖼️ bilder', "ct_csel": 'bruk tastane CTRL og SHIFT for markering av filer i ikonvising">merk', + "ct_dsel": 'klikk-og-dra for å merke filer i ikonvising">dra', "ct_dl": 'last ned filer (ikkje vis i nettleseren)">dl', "ct_ihop": 'bla ned åt sist viste bilde når bildevisaren lukkast">g⮯', "ct_dots": 'vis skjulte filer (gitt at serveren tillèt det)">.synlig', @@ -261,6 +262,7 @@ Ls.nno = { "cdt_ask": "vis knappar for å laste fleire filer nederst på sida i staden for å gradvis laste meir av mappea når man scroller ned", "cdt_hsort": "antall sorteringsreglar (<code>,sorthref</code>) som skal inkluderast når media-URL'ar genererast. Dersom denne er 0 så vil sorteringsreglar i URL'ar korkje bli generert eller lest", "cdt_ren": "slå på tilpassa høgreklikkmeny (den vanlege menyen er tilgjengeleg med shift + høgreklikk)", + "cdt_rdb": "høgreklikk to gonger for å vise den vanlege høgreklikkmenyen", "tt_entree": "bytt åt mappehierarki$NSnarvei: B", "tt_detree": "bytt åt tradisjonell stivising$NSnarvei: B", @@ -651,10 +653,13 @@ Ls.nno = { "rc_cut": "klipp ut", "rc_cpy": "kopier", "rc_pst": "Lim inn", + "rc_rnm": "endre namn", "rc_nfo": "ny mappe", "rc_nfi": "ny fil", "rc_sal": "vel alle", "rc_sin": "inverter val", + "rc_shf": "del denne mappa", + "rc_shs": "del markering", "lang_set": "passar det å laste sida på nytt?", diff --git a/copyparty/web/tl/nor.js b/copyparty/web/tl/nor.js index 128d2e3e..0f75ddc7 100644 --- a/copyparty/web/tl/nor.js +++ b/copyparty/web/tl/nor.js @@ -262,6 +262,7 @@ Ls.nor = { "cdt_ask": "vis knapper for å laste flere filer nederst på siden istedenfor å gradvis laste mer av mappen når man scroller ned", "cdt_hsort": "antall sorterings-regler (<code>,sorthref</code>) som skal inkluderes når media-URL'er genereres. Hvis denne er 0 så vil sorterings-regler i URL'er hverken bli generert eller lest", "cdt_ren": "bruk egendefinert høyreklikkmeny (den vanlige menyen er tilgjengelig med shift + høyreklikk)", + "cdt_rdb": "høyreklikk to ganger for å vise den vanlige høyreklikkmenyen", "tt_entree": "bytt til mappehierarki$NSnarvei: B", "tt_detree": "bytt til tradisjonell sti-visning$NSnarvei: B", @@ -652,10 +653,13 @@ Ls.nor = { "rc_cut": "klipp ut", "rc_cpy": "kopier", "rc_pst": "Lim inn", + "rc_rnm": "gi nytt navn", "rc_nfo": "ny mappe", "rc_nfi": "ny fil", "rc_sal": "velg alle", "rc_sin": "inverter utvalg", + "rc_shf": "del denne mappen", + "rc_shs": "del markering", "lang_set": "passer det å laste siden på nytt?", diff --git a/copyparty/web/tl/pol.js b/copyparty/web/tl/pol.js index 13c3a7de..76046af6 100644 --- a/copyparty/web/tl/pol.js +++ b/copyparty/web/tl/pol.js @@ -229,6 +229,7 @@ Ls.pol = { "ct_ttips": '◔ ◡ ◔">ℹ️ podpowiedzi', "ct_thumb": 'w widoku siatki, przełącz ikony i miniaturki$NSkrót: T">🖼️ miniaturki', "ct_csel": 'użyj CTRL i SHIFT do wybierania plików w widoku siatki">wybierz', + "ct_dsel": 'użyj zaznaczania przez przeciąganie w widoku siatki">przeciągnij', //m "ct_dl": 'wymuś pobieranie (nie wyświetlaj inline) po kliknięciu pliku">dl', //m "ct_ihop": 'przejdź do ostatniego pliku po zamknięciu przeglądarki obrazów">g⮯', "ct_dots": 'pokaż ukryte pliki (jeśli pozwala serwer)">ukryte', @@ -267,6 +268,7 @@ Ls.pol = { "cdt_ask": "przy przewijaniu w dół,$Nzapytaj co robić,$Nzamiast wczytywać kolejne pliki", "cdt_hsort": "ile zasad sortowania (<code>,sorthref</code>) zawierać w generowanych linkach multimediów. Wartość 0 sprawi, że zasady sortowania zawarte w linkach multimediów przy otwarciu również będą ignorowane", "cdt_ren": "włącz niestandardowe menu kontekstowe, standardowe menu jest dostępne po wciśnięciu shift i kliknięciu prawym przyciskiem", //m + "cdt_rdb": "pokaż standardowe menu prawego przycisku, gdy niestandardowe jest już otwarte i nastąpi ponowne kliknięcie", //m "tt_entree": "pokaż panel nawigacyjny (panel boczny z drzewem folderów)$NSkrót: B", "tt_detree": "pokaż ślad nawigacyjny$NSkrót: B", @@ -657,10 +659,13 @@ Ls.pol = { "rc_cut": "wytnij", //m "rc_cpy": "kopiuj", //m "rc_pst": "wklej", //m + "rc_rnm": "zmień nazwę", //m "rc_nfo": "nowy folder", //m "rc_nfi": "nowy plik", //m "rc_sal": "zaznacz wszystko", //m "rc_sin": "odwróć zaznaczenie", //m + "rc_shf": "udostępnij ten folder", //m + "rc_shs": "udostępnij zaznaczenie", //m "lang_set": "odśwież stronę (F5), aby zastosować zmianę.", diff --git a/copyparty/web/tl/por.js b/copyparty/web/tl/por.js index 53e54302..29aea56c 100644 --- a/copyparty/web/tl/por.js +++ b/copyparty/web/tl/por.js @@ -226,6 +226,7 @@ Ls.por = { "ct_ttips": '◔ ◡ ◔">ℹ️ dicas de ferramentas', "ct_thumb": 'na visualização de grade, alternar entre ícones ou miniaturas$NHotkey: T">🖼️ miniaturas', "ct_csel": 'usar CTRL e SHIFT para seleção de arquivo na visualização de grade">sel', + "ct_dsel": 'usar seleção por arrasto na visualização de grade">arrastar', //m "ct_dl": 'forçar download (não exibir inline) ao clicar em um arquivo">dl', //m "ct_ihop": 'quando o visualizador de imagens for fechado, rolar para o último arquivo visualizado">g⮯', "ct_dots": 'mostrar arquivos ocultos (se o servidor permitir)">dotfiles', @@ -264,6 +265,7 @@ Ls.por = { "cdt_ask": "ao rolar para o final,$nem vez de carregar mais arquivos,$nperguntar o que fazer", "cdt_hsort": "quantas regras de ordenação (<code>,sorthref</code>) incluir em URLs de mídia. Definir isso para 0 também ignorará as regras de ordenação incluídas em links de mídia quando você clicar neles", "cdt_ren": "ativar menu de clique direito personalizado, o menu normal permanece acessível com shift + clique direito", //m + "cdt_rdb": "mostrar o menu padrão do botão direito quando o personalizado já estiver aberto e houver novo clique", //m "tt_entree": "mostrar painel de navegação (árvore de diretórios)$NHotkey: B", "tt_detree": "mostrar breadcrumbs$NHotkey: B", @@ -654,10 +656,13 @@ Ls.por = { "rc_cut": "recortar", //m "rc_cpy": "copiar", //m "rc_pst": "colar", //m + "rc_rnm": "renomear", //m "rc_nfo": "nova pasta", //m "rc_nfi": "novo arquivo", //m "rc_sal": "selecionar tudo", //m "rc_sin": "inverter seleção", //m + "rc_shf": "compartilhar esta pasta", //m + "rc_shs": "compartilhar seleção", //m "lang_set": "atualizar para a mudança ter efeito?", diff --git a/copyparty/web/tl/rus.js b/copyparty/web/tl/rus.js index 549921e7..4c48ae1a 100644 --- a/copyparty/web/tl/rus.js +++ b/copyparty/web/tl/rus.js @@ -226,6 +226,7 @@ Ls.rus = { "ct_ttips": '◔ ◡ ◔">ℹ️ подсказки', "ct_thumb": 'переключение между иконками и миниатюрами в режиме сетки$NГорячая клавиша: T">🖼️ миниат.', "ct_csel": 'держите CTRL или SHIFT для выделения файлов в режиме сетки">выбор', + "ct_dsel": 'использовать выделение перетаскиванием в режиме сетки">перетащить', //m "ct_dl": 'принудительная загрузка (не показывать встроенно) при щелчке по файлу">dl', //m "ct_ihop": 'показывать последний открытый файл после закрытия просмотрщика изображений">g⮯', "ct_dots": 'показывать скрытые файлы (если есть доступ)">скрыт.', @@ -264,6 +265,7 @@ Ls.rus = { "cdt_ask": "внизу страницы спрашивать о действии вместо автоматической загрузки следующих файлов", "cdt_hsort": "сколько правил сортировки (<code>,sorthref</code>) включать в адрес страницы. Если значение равно 0, по нажатии на ссылки будут игнорироваться правила, включённые в них", "cdt_ren": "включить настраиваемое контекстное меню, обычное меню доступно при нажатии shift и правой кнопки мыши", //m + "cdt_rdb": "показывать обычное меню правого клика, если пользовательское уже открыто и выполняется повторный клик", //m "tt_entree": "показать панель навигации$NГорячая клавиша: B", "tt_detree": "скрыть панель навигации$NГорячая клавиша: B", @@ -654,10 +656,13 @@ Ls.rus = { "rc_cut": "вырезать", //m "rc_cpy": "копировать", //m "rc_pst": "вставить", //m + "rc_rnm": "переименовать", //m "rc_nfo": "новая папка", //m "rc_nfi": "новый файл", //m "rc_sal": "выбрать всё", //m "rc_sin": "инвертировать выделение", //m + "rc_shf": "поделиться этой папкой", //m + "rc_shs": "поделиться выделенным", //m "lang_set": "перезагрузить страницу, чтобы применить изменения?", diff --git a/copyparty/web/tl/spa.js b/copyparty/web/tl/spa.js index 2267aa87..508a639f 100644 --- a/copyparty/web/tl/spa.js +++ b/copyparty/web/tl/spa.js @@ -225,6 +225,7 @@ Ls.spa = { "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'en vista de cuadrícula, alternar iconos o miniaturas$NAtajo: T">🖼️ miniaturas', "ct_csel": 'usa CTRL y SHIFT para seleccionar archivos en la vista de cuadrícula">sel', + "ct_dsel": 'usa la selección por arrastre en la vista de cuadrícula">arrastrar', //m "ct_dl": 'forzar descarga (no mostrar en línea) al hacer clic en un archivo">dl', //m "ct_ihop": 'al cerrar el visor de imágenes, desplazarse hasta el último archivo visto">g⮯', "ct_dots": 'mostrar archivos ocultos (si el servidor lo permite)">archivos ocultos', @@ -263,6 +264,7 @@ Ls.spa = { "cdt_ask": "al llegar al final,$Nen lugar de cargar más archivos,$Npreguntar qué hacer", "cdt_hsort": "cuántas reglas de ordenación (<code>,sorthref</code>) incluir en las URLs de medios. Ponerlo a 0 también ignorará las reglas de ordenación incluidas en los enlaces de medios al hacer clic en ellos", "cdt_ren": "habilitar menú contextual personalizado, el menú normal sigue siendo accesible con shift + clic derecho", //m + "cdt_rdb": "mostrar el menú normal de clic derecho cuando el personalizado ya está abierto y se vuelve a hacer clic", //m "tt_entree": "mostrar panel de navegación (barra lateral con árbol de directorios)$NAtajo: B", "tt_detree": "mostrar breadcrumbs$NAtajo: B", @@ -653,10 +655,13 @@ Ls.spa = { "rc_cut": "cortar", //m "rc_cpy": "copiar", //m "rc_pst": "pegar", //m + "rc_rnm": "renombrar", //m "rc_nfo": "nueva carpeta", //m "rc_nfi": "nuevo archivo", //m "rc_sal": "seleccionar todo", //m "rc_sin": "invertir selección", //m + "rc_shf": "compartir esta carpeta", //m + "rc_shs": "compartir selección", //m "lang_set": "¿refrescar para que el cambio surta efecto?", diff --git a/copyparty/web/tl/swe.js b/copyparty/web/tl/swe.js index b5d4e8f9..6f5e9b0b 100644 --- a/copyparty/web/tl/swe.js +++ b/copyparty/web/tl/swe.js @@ -226,6 +226,7 @@ Ls.swe = { "ct_ttips": '◔ ◡ ◔">ℹ️ tips', "ct_thumb": 'växla mellan miniatyrer och ikoner i rutnätsvyn$NSnabbtangent: T">🖼️ miniatyrer', "ct_csel": 'använd CTRL och SKIFT för urval av filer i rutnätsvyn">val', + "ct_dsel": 'använd dra-urval i rutnätsvyn">dra', //m "ct_dl": 'tvinga nedladdning (visa inte inline) när en fil klickas">dl', //m "ct_ihop": 'skrolla till den senast visade filen när bildvisaren stängs">g⮯', "ct_dots": 'visa dolda filer (om servern tillåter detta)">dolda', @@ -264,6 +265,7 @@ Ls.swe = { "cdt_ask": "när du når botten av vyn,$Nbe om en åtgärd istället för att ladda fler filer", "cdt_hsort": "hur många sorteringsregler (<code>,sorthref</code>) att inkludera i media-URL:er. Sätts detta till 0 kommer regler i klickade medialänkar även att ignoreras", "cdt_ren": "aktivera anpassad högerklicksmeny, den vanliga menyn är tillgänglig med shift + högerklick", //m + "cdt_rdb": "visa den vanliga högerklicksmenyn när den anpassade redan är öppen och man högerklickar igen", //m "tt_entree": "visa trädvy$NSnabbtangent: B", "tt_detree": "visa brödsmulor$NSnabbtangent: B", @@ -654,10 +656,13 @@ Ls.swe = { "rc_cut": "klipp ut", //m "rc_cpy": "kopiera", //m "rc_pst": "klistra in", //m + "rc_rnm": "byt namn", //m "rc_nfo": "ny mapp", //m "rc_nfi": "ny fil", //m "rc_sal": "markera alla", //m "rc_sin": "invertera markering", //m + "rc_shf": "dela denna mapp", //m + "rc_shs": "dela urval", //m "lang_set": "uppdatera för att ändringen ska ta effekt?", diff --git a/copyparty/web/tl/tur.js b/copyparty/web/tl/tur.js index 3034b283..0efaa2d5 100644 --- a/copyparty/web/tl/tur.js +++ b/copyparty/web/tl/tur.js @@ -226,6 +226,7 @@ Ls.tur = { "ct_ttips": '◔ ◡ ◔">ℹ️ ipuçları', "ct_thumb": 'ızgara görünümünde, simgeler ve küçük resimler arasında geçiş yapın$NKısayol: T">🖼️ küçük resimler', "ct_csel": 'ızgara görünümünde dosya seçimi için CTRL ve SHIFT tuşlarını kullanın">seç', + "ct_dsel": 'ızgara görünümünde sürükleyerek seçimi kullanın">sürükle', //m "ct_dl": 'dosyaya tıklandığında indirmeyi zorla (satır içinde görüntüleme)">dl', //m "ct_ihop": 'resim görüntüleyici kapatıldığında, en son görüntülenen dosyaya kaydırın">g⮯', "ct_dots": 'gizli dosyaları göster (sunucu izin veriyorsa)">nokta dosyaları', @@ -264,6 +265,7 @@ Ls.tur = { "cdt_ask": "aşağı kaydırırken,$Ndaha fazla dosya yüklemek yerine,$Nne yapılacağını sor", "cdt_hsort": "medya-URL'lerinde dahil edilecek sıralama kurallarının sayısı (<code>,sorthref</code>). Bunu 0 olarak ayarlamak, tıklanırken medya bağlantılarına dahil edilen sıralama kurallarını da yok sayacaktır", "cdt_ren": "özel sağ tık menüsünü etkinleştir, normal menü shift + sağ tık ile erişilebilir", //m + "cdt_rdb": "özel menü zaten açıkken tekrar sağ tıklanınca normal sağ tık menüsünü göster", //m "tt_entree": "navigasyon panosunu göster (yan dizin panosu)$NHotkey: B", "tt_detree": "içerik haritasını göster$Kısayol: B", @@ -654,10 +656,13 @@ Ls.tur = { "rc_cut": "kes", //m "rc_cpy": "kopyala", //m "rc_pst": "yapıştır", //m + "rc_rnm": "yeniden adlandır", //m "rc_nfo": "yeni klasör", //m "rc_nfi": "yeni dosya", //m "rc_sal": "tümünü seç", //m "rc_sin": "seçimi tersine çevir", //m + "rc_shf": "bu klasörü paylaş", //m + "rc_shs": "seçimi paylaş", //m "lang_set": "Değişikliklerin etki göstermesi için sayfa yenilensin mi?", diff --git a/copyparty/web/tl/ukr.js b/copyparty/web/tl/ukr.js index 33ee9f0c..2265c2ec 100644 --- a/copyparty/web/tl/ukr.js +++ b/copyparty/web/tl/ukr.js @@ -226,6 +226,7 @@ Ls.ukr = { "ct_ttips": '◔ ◡ ◔">ℹ️ підказки', "ct_thumb": 'у режимі сітки, перемкнути іконки або мініатюри$NГаряча клавіша: T">🖼️ мініатюри', "ct_csel": 'використовувати CTRL і SHIFT для вибору файлів у режимі сітки">вибір', + "ct_dsel": 'використовувати вибір перетягуванням у режимі сітки">перетягнути', //m "ct_dl": 'примусове завантаження (не показувати вбудовано) під час натискання на файл">dl', //m "ct_ihop": 'коли переглядач зображень закрито, прокрутити вниз до останнього переглянутого файлу">g⮯', "ct_dots": 'показати приховані файли (якщо сервер дозволяє)">приховані файли', @@ -264,6 +265,7 @@ Ls.ukr = { "cdt_ask": "при прокрутці до низу,$Nзамість завантаження більше файлів,$Nзапитати, що робити", "cdt_hsort": "скільки правил сортування (<code>,sorthref</code>) включати в медіа-URL. Встановлення цього в 0 також буде ігнорувати правила сортування, включені в медіа посилання при їх натисканні", "cdt_ren": "увімкнути користувацьке контекстне меню, звичайне меню доступне при натисканні shift і правої кнопки миші", //m + "cdt_rdb": "показувати звичайне меню правої кнопки, якщо власне меню вже відкрите і відбувається повторне клацання", //m "tt_entree": "показати панель навігації (бічна панель дерева каталогів)$NГаряча клавіша: B", "tt_detree": "показати хлібні крихти$NГаряча клавіша: B", @@ -654,10 +656,13 @@ Ls.ukr = { "rc_cut": "вирізати", //m "rc_cpy": "копіювати", //m "rc_pst": "вставити", //m + "rc_rnm": "перейменувати", //m "rc_nfo": "нова папка", //m "rc_nfi": "новий файл", //m "rc_sal": "вибрати все", //m "rc_sin": "інвертувати вибір", //m + "rc_shf": "поділитися цією папкою", //m + "rc_shs": "поділитися вибраним", //m "lang_set": "оновити сторінку, щоб зміни набули чинності?", diff --git a/copyparty/web/tl/vie.js b/copyparty/web/tl/vie.js index 2932a44d..4805c0ef 100644 --- a/copyparty/web/tl/vie.js +++ b/copyparty/web/tl/vie.js @@ -227,6 +227,7 @@ Ls.vie = { "ct_ttips": '༼ ◕_◕ ༽">ℹ️ tooltips', "ct_thumb": 'ở chế độ lưới, chuyển biểu tượng hoặc hình thu nhỏ$NPhím tắt: T">🖼️ ảnh thu nhỏ', "ct_csel": 'dùng CTRL và SHIFT để chọn tệp trong chế độ lưới">sel', + "ct_dsel": 'dùng chọn bằng cách kéo trong chế độ lưới">kéo', //m "ct_dl": 'cưỡng chế tải xuống (không hiện thị trong dòng) khi nhấp vào tệp">dl', "ct_ihop": 'khi đóng trình xem ảnh, cuộn xuống tệp đã xem gần nhất">g⮯', "ct_dots": 'hiển thị tệp ẩn (nếu máy chủ cho phép)">dotfiles', @@ -268,6 +269,7 @@ Ls.vie = { "cdt_ask": "khi cuộn xuống cuối,$Nthay vì tải thêm tệp,$Nhỏi người dùng muốn làm gì", "cdt_hsort": "số lượng luật sắp xếp(<code>,sorthref</code>) được đưa vào URL media. Đặt bằng 0 cũng sẽ bỏ qua các quy tắc sắp xếp trong liên kết media khi nhấp vào chúng", "cdt_ren": "bật menu chuột phải tùy chỉnh, menu mặc định vẫn truy cập được bằng shift + chuột phải", //m + "cdt_rdb": "hiển thị menu chuột phải thông thường khi menu tùy chỉnh đã mở và nhấp chuột phải lần nữa", //m "tt_entree": "hiển thị thanh điều hướng (cây thư mục)$NPhím tắt: B", "tt_detree": "hiển thị đường dẫn$NPhím tắt: B", @@ -687,10 +689,13 @@ Ls.vie = { "rc_cut": "cắt", //m "rc_cpy": "sao chép", //m "rc_pst": "dán", //m + "rc_rnm": "đổi tên", //m "rc_nfo": "thư mục mới", //m "rc_nfi": "tệp mới", //m "rc_sal": "chọn tất cả", //m "rc_sin": "đảo ngược lựa chọn", //m + "rc_shf": "chia sẻ thư mục này", //m + "rc_shs": "chia sẻ lựa chọn", //m "lang_set": "tải lại trang để áp dụng thay đổi ngôn ngữ", diff --git a/scripts/tl.js b/scripts/tl.js index 0b80c69c..d2b54ab3 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -690,6 +690,8 @@ Ls.hmn = { "rc_nfi": "new file", "rc_sal": "select all", "rc_sin": "invert selection", + "rc_shf": "share this folder", + "rc_shs": "share selection", "lang_set": "refresh to make the change take effect?", From e752005543e08ad7e462015c8e3f9c4bd3eaf564 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 19 Jan 2026 00:28:00 +0000 Subject: [PATCH 29/54] rcm: config sets defaults; also rename ren/rdb to reduce probability of localstorage conflicts with other softwares --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 1 + copyparty/web/browser.js | 11 ++++------- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 4db2fcc3..f1635289 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1833,6 +1833,7 @@ def add_ui(ap, retry: int): ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC") ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)") + ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,)) ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 0bd70958..96f9bd1b 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -3201,6 +3201,7 @@ class AuthSrv(object): "dsort": vf["sort"], "dcrop": vf["crop"], "dth3x": vf["th3x"], + "drcm": self.args.rcm, "dvol": self.args.au_vol, "idxh": int(self.args.ih), "dutc": not self.args.localtime, diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 372f72e9..77e62c1a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -997,7 +997,7 @@ ebi('op_cfg').innerHTML = ( '
\n' + '
\n' + '

' + L.cl_keytype + '

\n' + - (!MOBILE ? '

' + L.cl_rcm + '

' : '') + + (!MOBILE ? '

' + L.cl_rcm + '

' : '') + '

' + L.cl_hiddenc + '  ' + (MOBILE ? '' + L.cl_hidec + ' / ' : '') + '' + L.cl_reset + '

' ); @@ -9494,12 +9494,9 @@ var rcm = (function () { if (MOBILE) return {enabled: false} - var r = { - enabled: true, - double: false - }; - bcfg_bind(r, 'enabled', 'ren', true); - bcfg_bind(r, 'double', 'rdb', false); + var r = {}; + bcfg_bind(r, 'enabled', 'rcm_en', drcm.charAt(0)=='y'); + bcfg_bind(r, 'double', 'rcm_db', drcm.charAt(1)=='y'); var menu = ebi('rcm'); var nsFile = { From cab9feb225c843bfbf71a07d136b7394460d1847 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 19 Jan 2026 01:26:37 +0000 Subject: [PATCH 30/54] v1.20.2 --- README.md | 5 ++++- copyparty/__version__.py | 4 ++-- copyparty/authsrv.py | 10 +++++++++- copyparty/web/browser.js | 2 ++ docs/changelog.md | 33 +++++++++++++++++++++++++++++++++ tests/util.py | 2 +- 6 files changed, 51 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f38d54db..151593f4 100644 --- a/README.md +++ b/README.md @@ -1882,7 +1882,7 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy ### metadata from xattrs -unix extended file attributes can be indexed into the db and made searchable; +unix extended file attributes (Linux-only) can be indexed into the db and made searchable; * `--db-xattr user.foo,user.bar` will index the xattrs `user.foo` and `user.bar`, * `--db-xattr user.foo=foo,user.bar=bar` will index them with the names `foo` and `bar`, @@ -1893,6 +1893,9 @@ however note that the tags must also be enabled with `-mte` so here are some com * `-e2ts --db-xattr user.foo,user.bar -mte +user.foo,user.bar` * `-e2ts --db-xattr user.foo=foo,user.bar=bar -mte +foo,bar` +as for actually adding the xattr `user.foo` to a file in the first place, +* `setfattr -n user.foo -v 'utsikt fra fløytoppen' photo.jpg` + ## file parser plugins diff --git a/copyparty/__version__.py b/copyparty/__version__.py index ec5f5dd7..0511f4c3 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 20, 1) +VERSION = (1, 20, 2) CODENAME = "sftp is fine too" -BUILD_DT = (2026, 1, 9) +BUILD_DT = (2026, 1, 19) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 96f9bd1b..0a6b1d17 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2117,15 +2117,23 @@ class AuthSrv(object): if errors: sys.exit(1) for vol in dropvols: - vol.realpath = "" vol.axs = AXS() vol.uaxs = {} vfs.all_vols.pop(vol.vpath, None) vfs.all_nodes.pop(vol.vpath, None) for zv in vfs.all_nodes.values(): + try: + zv.all_aps.remove(vol.realpath) + zv.all_vps.remove(vol.vpath) + # pointless but might as well: + zv.all_vols.pop(vol.vpath) + zv.all_nodes.pop(vol.vpath) + except: + pass zs = next((x for x, y in zv.nodes.items() if y == vol), "") if zs: zv.nodes.pop(zs) + vol.realpath = "" promote = [] demote = [] diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 77e62c1a..78bcbaa2 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -2,6 +2,8 @@ var J_BRW = 1; +if (!window.drcm) alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser'); + var XHR = XMLHttpRequest, img_re = /\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i; diff --git a/docs/changelog.md b/docs/changelog.md index ac067cf0..3a09f4d2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,36 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2026-0109-0052 `v1.20.1` sftp fixes + +## 🧪 new features + +* #1174 add Japanese translation (thx @tkymmm!) b918b592 +* #1164 rightclick-menu now works in the gridview too (thx @Foox-dev!) feabbf3e +* #1176 IP to bind can be specified per protocol 87a5c22a + +## 🩹 bugfixes + +* various SFTP fixes (i blame the single [tschunk](https://germanfoods.org/tschunk/) on day 3): + * #1170 be more lenient regarding `stat` permissions 90308284 + * #1170 deletes could return EPERM when ENOENT was more appropriate 8c9e1016 + * #1170 files would be created with an extremely restrictive chmod 2f4a30b6 + * certified octal moment + * write-only folders could return ENOENT 6c41bac6 +* #1177 disk-usage quotas became incompatible with shares in v1.20.0 038af507 +* appending to existing files with `?apnd` was possible in volumes with non-reflink dedup, where it could propagate to deduped copies of the file 738a419b + * (was only possible for users with write+delete perms, so at least it couldn't be used for nefarious purposes) +* rightclick-menu: "copy link" would strip filekeys 3a16d346 + +## 🔧 other changes + +* copyparty.exe: updated pillow to 12.1.0 a9ae6d51 + +## 🌠 fun facts + +* [tschunk](https://germanfoods.org/tschunk/) is the sound a hacker makes as they faceplant onto the table after having one too many + * also see the funfacts in [the previous release](https://github.com/9001/copyparty/releases/tag/v1.20.0) for more CCC hijinks :p + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2026-0102-0007 `v1.20.0` sftp is fine too diff --git a/tests/util.py b/tests/util.py index 3afba6bc..245c6916 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 db_xattr 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 shr_site site tcolor textfiles txt_eol ufavico ufavico_h unlist up_site vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) - ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary 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 http_vary rcm 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" From 78f6855f08a210ded0eeb34da9eafb9cc2de024b Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 19 Jan 2026 01:31:57 +0000 Subject: [PATCH 31/54] update pkgs to 1.20.2 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/makedeb-mpr/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index fa8237d1..e7c9053b 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -3,7 +3,7 @@ # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead. pkgname=copyparty -pkgver="1.20.1" +pkgver="1.20.2" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -24,7 +24,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("etc/${pkgname}/copyparty.conf" ) -sha256sums=("4f513ca9e3d1c11a7bb4e1a8a925dda2449b9565e91f6ef7cbe10367fa4e2935") +sha256sums=("f4fc7a53a6d23163426f4f0f407c7aa53984e0b7a7b0b33700464bc7d7f3bd95") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 0941869c..b430a0a1 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.20.1 +pkgver=1.20.2 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("/etc/${pkgname}.d/init" ) -sha256sums=("4f513ca9e3d1c11a7bb4e1a8a925dda2449b9565e91f6ef7cbe10367fa4e2935") +sha256sums=("f4fc7a53a6d23163426f4f0f407c7aa53984e0b7a7b0b33700464bc7d7f3bd95") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index e15e49ed..3eb5e29c 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.20.1/copyparty-1.20.1.tar.gz", - "version": "1.20.1", - "hash": "sha256-T1E8qePRwRp7tOGoqSXdokSblWXpH273y+EDZ/pOKTU=" + "url": "https://github.com/9001/copyparty/releases/download/v1.20.2/copyparty-1.20.2.tar.gz", + "version": "1.20.2", + "hash": "sha256-9Px6U6bSMWNCb08PQHx6pTmE4LensLM3AEZLx9fzvZU=" } \ No newline at end of file From b4df8fa23cf71f4c0f8139ecd96121f1ef600839 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Jan 2026 03:19:32 +0000 Subject: [PATCH 32/54] multipart-parser correctness (closes #1227); makes it possible to login from the webbrowser dillo; * unlike every other browser, dillo does NOT send a trailing "\r\n" after the terminating "--"; turns out that dillo got this right and every other browser didn't, fun * dillo announces the boundary in quotes, which is spec-optional the multipart-parser is now 2% slower --- copyparty/util.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/copyparty/util.py b/copyparty/util.py index 3cbdf3d1..1b9b5431 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -961,11 +961,13 @@ class _Unrecv(object): self.s = s self.log = log self.buf: bytes = b"" + self.nb = 0 def recv(self, nbytes: int, spins: int = 1) -> bytes: if self.buf: ret = self.buf[:nbytes] self.buf = self.buf[nbytes:] + self.nb += len(ret) return ret while True: @@ -985,6 +987,7 @@ class _Unrecv(object): if not ret: raise UnrecvEOF("client stopped sending data") + self.nb += len(ret) return ret def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes: @@ -1012,6 +1015,7 @@ class _Unrecv(object): def unrecv(self, buf: bytes) -> None: self.buf = buf + self.buf + self.nb -= len(buf) # !rm.yes> @@ -1024,6 +1028,7 @@ class _LUnrecv(object): self.s = s self.log = log self.buf = b"" + self.nb = 0 def recv(self, nbytes: int, spins: int) -> bytes: if self.buf: @@ -1031,6 +1036,7 @@ class _LUnrecv(object): self.buf = self.buf[nbytes:] t = "\033[0;7mur:pop:\033[0;1;32m {}\n\033[0;7mur:rem:\033[0;1;35m {}\033[0m" print(t.format(ret, self.buf)) + self.nb += len(ret) return ret ret = self.s.recv(nbytes) @@ -1039,6 +1045,7 @@ class _LUnrecv(object): if not ret: raise UnrecvEOF("client stopped sending data") + self.nb += len(ret) return ret def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes: @@ -1067,6 +1074,7 @@ class _LUnrecv(object): def unrecv(self, buf: bytes) -> None: self.buf = buf + self.buf + self.nb -= len(buf) t = "\033[0;7mur:push\033[0;1;31m {}\n\033[0;7mur:rem:\033[0;1;35m {}\033[0m" print(t.format(buf, self.buf)) @@ -1807,6 +1815,11 @@ class MultipartParser(object): self.log = log_func self.args = args self.headers = http_headers + try: + self.clen = int(http_headers["content-length"]) + sr.nb = 0 + except: + self.clen = 0 self.re_ctype = RE_CTYPE self.re_cdisp = RE_CDISP @@ -1962,7 +1975,10 @@ class MultipartParser(object): if tail == b"--": # EOF indicated by this immediately after final boundary - tail = self.sr.recv_ex(2, False) + if self.clen == self.sr.nb: + tail = b"\r\n" # dillo doesn't terminate with trailing \r\n + else: + tail = self.sr.recv_ex(2, False) run = False if tail != b"\r\n": @@ -1980,6 +1996,8 @@ class MultipartParser(object): def parse(self) -> None: boundary = get_boundary(self.headers) + if boundary.startswith('"') and boundary.endswith('"'): + boundary = boundary[1:-1] # dillo uses quotes self.log("boundary=%r" % (boundary,)) # spec says there might be junk before the first boundary, From ba67b279463530742cd7f1f1adcb738e66dad8aa Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Jan 2026 03:19:41 +0000 Subject: [PATCH 33/54] no racing --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 785f85ac..e197c122 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -997,8 +997,8 @@ class HttpCli(object): rt = bans[ip] - time.time() if rt < 0: - self.log("client unbanned", 3) del bans[ip] + self.log("client unbanned", 3) return False self.log("banned for {:.0f} sec".format(rt), 6) From 0a3a80725aecc9303782cdbf8afcfc12937b7c1d Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Jan 2026 03:24:00 +0000 Subject: [PATCH 34/54] fix jumpvol ?ls v2 the missing part of 66750391ae71cb284f61d8c8e6de3ec807714843 --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index e197c122..b63abace 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6490,7 +6490,7 @@ class HttpCli(object): zs = "%s/" % (self.vpath,) vols = [(x[len(zs) :], y) for x, y in vols if x.startswith(zs)] vols = [(x.split("/", 1)[0], y) for x, y in vols] - vols = [x for x in vols if x[0]] + vols = list(({x: y for x, y in vols if x}).items()) if not vols and self.vpath: return self.tx_404(True) dirs = [ From 6dcb1efb7c5efc804e5039cd78a7d3377fc2bbc3 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Jan 2026 04:55:50 +0000 Subject: [PATCH 35/54] add ?smsg --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 37 +++++++++++++++++++++++++++++++++++++ copyparty/svchub.py | 6 ++++++ docs/devnotes.md | 1 + tests/util.py | 2 +- 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f1635289..1ae4eec8 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1562,6 +1562,7 @@ def add_optouts(ap): ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI") ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI. This is the same as --du-who no") ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI") + ap2.add_argument("--smsg", metavar="T,T", type=u, default="POST", help="HTTP-methods to allow ?smsg for; will execute xm hooks like urlform / message-to-serverlog; dangerous example: [\033[32mGET,POST\033[0m]. \033[1;31mWARNING:\033[0m The default (POST) is safe, but GET is dangerous; security/CSRF hazard") ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)") ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)") ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index b63abace..b861aae9 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1434,6 +1434,9 @@ class HttpCli(object): self.uparam["h"] = "" + if "smsg" in self.uparam: + return self.handle_smsg() + if "tree" in self.uparam: return self.tx_tree() @@ -2246,6 +2249,9 @@ class HttpCli(object): ): return self.handle_post_json() + if "smsg" in self.uparam: + return self.handle_smsg() + if "move" in self.uparam: return self.handle_mv() @@ -2332,6 +2338,37 @@ class HttpCli(object): raise Pebkac(405, "don't know how to handle POST(%r)" % (ctype,)) + def handle_smsg(self) -> bool: + if self.mode not in self.args.smsg_set: + raise Pebkac(403, "smsg is disabled for this http-method in server config") + + msg = self.uparam["smsg"] + self.log("smsg %d @ %r\n %r\n" % (len(msg), "/" + self.vpath, msg)) + + xm = self.vn.flags.get("xm") + if xm: + xm_rsp = runhook( + self.log, + self.conn.hsrv.broker, + None, + "xm", + xm, + self.vn.canonical(self.rem), + self.vpath, + self.host, + self.uname, + self.asrv.vfs.get_perms(self.vpath, self.uname), + time.time(), + len(msg), + self.ip, + time.time(), + [msg, msg], + ) + self.loud_reply(xm_rsp.get("stdout") or "", status=202) + else: + self.loud_reply("k", status=202) + return True + def get_xml_enc(self, txt: str) -> str: ofs = txt[:512].find(' encoding="') enc = "" diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 3fd30790..39c0491a 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1118,6 +1118,12 @@ class SvcHub(object): vsa = [x.lower() for x in vsa if x] setattr(al, k + "_set", set(vsa)) + for k in "smsg".split(" "): + vs = getattr(al, k) + vsa = [x.strip() for x in vs.split(",")] + vsa = [x.upper() for x in vsa if x] + setattr(al, k + "_set", set(vsa)) + zs = "dav_ua1 sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip" for k in zs.split(" "): vs = getattr(al, k) diff --git a/docs/devnotes.md b/docs/devnotes.md index 652b0d33..89dc8fb3 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -289,6 +289,7 @@ upload modifiers: | GET | `?imgs=0` | ui: show list-view | | GET | `?thumb` | ui, grid-mode: show thumbnails | | GET | `?thumb=0` | ui, grid-mode: show icons | +| POST | `?smsg=foo` | send-msg-to-serverlog / run xm hook | # event hooks diff --git a/tests/util.py b/tests/util.py index 245c6916..a665c090 100644 --- a/tests/util.py +++ b/tests/util.py @@ -164,7 +164,7 @@ class Cfg(Namespace): ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ka.update(**{k: 0 for k in ex.split()}) - ex = "ah_alg bname chdir chmod_f chpw_db db_xattr 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 shr_site site tcolor textfiles txt_eol ufavico ufavico_h unlist up_site vname xff_src zipmaxt R RS SR" + ex = "ah_alg bname chdir chmod_f chpw_db db_xattr 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 shr_site site smsg tcolor textfiles txt_eol ufavico ufavico_h unlist up_site vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rcm rss_fmt_d rss_fmt_t spinner" From c41ee3fc27710b85e5f21359c7170bdd649f28e2 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Jan 2026 05:05:50 +0000 Subject: [PATCH 36/54] v1.20.3 --- copyparty/__version__.py | 4 ++-- docs/changelog.md | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 0511f4c3..4c43277a 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 20, 2) +VERSION = (1, 20, 3) CODENAME = "sftp is fine too" -BUILD_DT = (2026, 1, 19) +BUILD_DT = (2026, 1, 21) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/docs/changelog.md b/docs/changelog.md index 3a09f4d2..03b5d239 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,53 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2026-0119-0126 `v1.20.2` xattrs + range-select + +## 🧪 new features + +* #1212, #1214 range-select in the grid-view by click-and-drag (thx @icxes!) 3e3228e0 72c59405 +* #134 xattrs (linux extended file attributes) can now be indexed and searchable 8240ef61 +* rightclick-menu: + * #1184 add rename option (thx @stackxp!) 25a8b96f + * #1216 add sharing options (thx @stackxp!) ffb25603 + * #1198, #1206 also works in the search-results view (thx @hackysphere!) 04f612ff d32704ed +* option to override the domain in certain links, so copyparty returns an external URL even if you're accessing it by a LAN address: + * #1211 newly created shares 41d3bae9 + * #255 newly uploaded files d9255538 +* new option `vol-nospawn` (volflag `nospawn`) to *not* automatically create the volume's folder on the server's HDD if it doesn't exist +* new option `vol-or-crash` (volflag `assert_root`) to intentionally crash on startup if a volume's folder doesn't already exist on the server HDD +* new option `--flo` to tweak the log-format used by the `-lo` option for logging to a file 826e84c8 +* #1197 u2c ([commandline uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)): give up and crash if server is offline for longer than 3 minutes (configurable) 67c5d8da + +## 🩹 bugfixes + +* #1203 configured chmod/chown rules were not applied when a file was being deduped bef07720 +* the `unlistc*` volflags could not be specified for single-file volumes 26648911 +* the defensive renaming of uploaded readmes/logues would assume the default filenames, not considering the recently added option to customize these names c17c3be0 +* #1191 the `ipu` option can once again be used to reject connections from certain IP-ranges caf831fc + * this was a regression in v1.19.21 causing the server to crash on startup if such a config was attempted +* some empty folders could be created during startup in certain server-configs with nested volumes 4e67b467 +* api: trying to `?ls` nested virtual folders could return an error 66750391 +* ui/ux: + * #1179 improve errormessage if audio transcoding fails 7357d46f + * ensure a trailing slash when viewing a folder with the `h` permission; good for relative links in html-files + +## 🔧 other changes + +* #1193, #1194: NixOS improvements (thx @toast003!) 9d223d6c d5a8a34b +* truncate huge errormessages from ffmpeg so the log doesn't get flooded 3aebfabd +* ui/ux: + * the `dl` button (to download selected files individually) now skips folders, since that never worked bc24604a + * #1200 add html classes to make custom styling easier c46cd7f5 + * rephrase errormessages from `see serverlog` to `see fileserver log` +* docs: + * mention in the readme that uploading files from a deeply nested folder using a webbrowser on Windows can fail because browsers don't handle the max-pathlen limitation of Windows optimally (not a copyparty-specific issue, but still hits us) + +## 🌠 fun facts + +* n/a; no fun has been had since [v1.20.0](https://github.com/9001/copyparty/releases/tag/v1.20.0) + * (that's a lie btw, [sniffing the airwaves](https://a.ocv.me/pub/g/2026/01/PXL_20260117_192619830.jpg?cache) *is* pretty darn fun 😁) + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2026-0109-0052 `v1.20.1` sftp fixes From dc8c229bcdc49419f16c983fe1f97d9d1e57bde2 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Jan 2026 05:09:47 +0000 Subject: [PATCH 37/54] update pkgs to 1.20.3 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/makedeb-mpr/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index e7c9053b..93cd9023 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -3,7 +3,7 @@ # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead. pkgname=copyparty -pkgver="1.20.2" +pkgver="1.20.3" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -24,7 +24,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("etc/${pkgname}/copyparty.conf" ) -sha256sums=("f4fc7a53a6d23163426f4f0f407c7aa53984e0b7a7b0b33700464bc7d7f3bd95") +sha256sums=("894e2857852ddbbc8b9560732be1084f70010cdf1c5aac18ba9f72699f8ec933") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index b430a0a1..6b0f323f 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.20.2 +pkgver=1.20.3 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("/etc/${pkgname}.d/init" ) -sha256sums=("f4fc7a53a6d23163426f4f0f407c7aa53984e0b7a7b0b33700464bc7d7f3bd95") +sha256sums=("894e2857852ddbbc8b9560732be1084f70010cdf1c5aac18ba9f72699f8ec933") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 3eb5e29c..42b6dacd 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.20.2/copyparty-1.20.2.tar.gz", - "version": "1.20.2", - "hash": "sha256-9Px6U6bSMWNCb08PQHx6pTmE4LensLM3AEZLx9fzvZU=" + "url": "https://github.com/9001/copyparty/releases/download/v1.20.3/copyparty-1.20.3.tar.gz", + "version": "1.20.3", + "hash": "sha256-iU4oV4Ut27yLlWBzK+EIT3ABDN8cWqwYup9yaZ+OyTM=" } \ No newline at end of file From e1eff2162373fa1d86557ae029daacfd49adcc6b Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 22 Jan 2026 23:54:42 +0000 Subject: [PATCH 38/54] no keepalive when proxied (#1231); might help prevent desync --- copyparty/httpcli.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index b861aae9..6917dec0 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -285,12 +285,6 @@ class HttpCli(object): uname = self.asrv.iacct.get(b) or self.asrv.sesa.get(b) return "%s\033[7m %s \033[27m%s" % (a, uname, c) - def _check_nonfatal(self, ex: Pebkac, post: bool) -> bool: - if post: - return ex.code < 300 - - return ex.code < 400 or ex.code in [404, 429] - def _assert_safe_rem(self, rem: str) -> None: # sanity check to prevent any disasters # (this function hopefully serves no purpose; validation has already happened at this point, this only exists as a last-ditch effort just in case) @@ -403,6 +397,7 @@ class HttpCli(object): trusted_xff = False n = self.args.rproxy if n: + self.keepalive = False zso = self.headers.get(self.args.xff_hdr) if zso: if n > 0: @@ -889,8 +884,8 @@ class HttpCli(object): self.terse_reply(b"", 500) return False - post = self.mode in ["POST", "PUT"] or "content-length" in self.headers - if not self._check_nonfatal(pex, post): + post = self.mode in ("POST", "PUT") or "content-length" in self.headers + if pex.code >= (300 if post else 400): self.keepalive = False em = str(ex) @@ -2213,7 +2208,7 @@ class HttpCli(object): raise Pebkac(403 if self.pw else 401, t % (self.uname, self.vn.vpath)) if not self.args.no_dav and self._applesan(): - return self.headers.get("content-length") == "0" + return False if self.headers.get("expect", "").lower() == "100-continue": try: From ffca67f25ac2b4a45b49d269be330b4988779b4e Mon Sep 17 00:00:00 2001 From: Diego Passos Couto <12452268+SpaceXCheeseWheel@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:06:11 -0700 Subject: [PATCH 39/54] rcm: new file/folder in gridview (#1235) enables creation of files/folders in grid-view --------- Co-authored-by: ed --- copyparty/web/browser.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 78bcbaa2..0bbd32b1 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -9514,11 +9514,20 @@ var rcm = (function () { var selFile = jcp(nsFile); function mktemp(is_dir) { - var row = mknod('tr', 'temp', - '-new-' + - '' - ); - QS("#files tbody").appendChild(row); + qsr('#rcm_tmp'); + if (!thegrid.en) { + var row = mknod('tr', 'rcm_tmp', + '-new-'); + QS("#files tbody").appendChild(row); + } + else { + var row = mknod('a', 'rcm_tmp', + ''); + if (is_dir) + row.className = 'dir'; + row.style.display = 'flex'; + QS("#ggrid").appendChild(row); + } function sendit(name) { name = ('' + name).trim(); From 1142ac25639cc67f5338c013636bcc209f990916 Mon Sep 17 00:00:00 2001 From: /dev/urandom <53902042+slashdevslashurandom@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:14:30 +0300 Subject: [PATCH 40/54] Esperanto translation update (#1229) Important changes: The "shift" key is called "MAJ" (short for "majuskla" for "uppercase") in the translation, so the new lines have been updated. "beligi json" is technically a valid string, but the "-on" ending could be confused for a grammatical ending, with the whole string being interpreted as "beautify javascript" instead, so instead "JSON" is capitalized and another "on" ending is added. A few commas are added for readability. Signed-off-by: /dev/urandom <53902042+slashdevslashurandom@users.noreply.github.com> --- copyparty/web/tl/epo.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/copyparty/web/tl/epo.js b/copyparty/web/tl/epo.js index af497407..c55ba251 100644 --- a/copyparty/web/tl/epo.js +++ b/copyparty/web/tl/epo.js @@ -84,8 +84,8 @@ Ls.epo = { ["M", "fermi dosieron"], ["E", "redakti dosieron"], ["S", "elekti dosieron (por eltondado/kopiado/alinomado)"], - ["Y", "elŝuti tekstodosieron"], //m - ["⇧ J", "beligi json"], //m + ["Y", "elŝuti tekstodosieron"], + ["⇧ J", "beligi JSONon"], ] ], @@ -118,7 +118,7 @@ Ls.epo = { "ot_unpost": "unpost: forigi viaj plej lastaj alŝutoj, aŭ ĉesigi nefinigitajn", "ot_bup": "bup: fundamenta alŝutilo, funkias eĉ kun netscape 4.0", "ot_mkdir": "mkdir: krei novan dosierujon", - "ot_md": "new-file: krei novan tekstodosieron", //m + "ot_md": "new-file: krei novan tekstodosieron", "ot_msg": "msg: sendi mesaĝon al servila protokolo", "ot_mp": "agordoj de medialudilo", "ot_cfg": "aliaj agordoj", @@ -127,7 +127,7 @@ Ls.epo = { "ot_noie": 'Bonvolu uzi retumilojn Chrome / Firefox / Edge', "ab_mkdir": "krei dosierujon", - "ab_mkdoc": "krei tekstodosieron", //m + "ab_mkdoc": "krei tekstodosieron", "ab_msg": "sendi mesaĝon al protokolo", "ay_path": "iri al dosierujoj", @@ -155,7 +155,7 @@ Ls.epo = { "ul_par": "paralelaj alŝutoj:", "ut_rand": "hazardigi dosiernomojn", "ut_u2ts": "kopii la tempon de lasta modifo$Nel via dosiersistemo al la servilo\">📅", - "ut_ow": "ĉu anstataŭigi dosierojn ĉe la servilo?$N🛡️: neniam (dosiero estos alŝutita kun nova dosiernomo)$N🕒: anstataŭigi, se servila dosiero estas pli malnova ol via$N♻️: ĉiam anstataŭigi, se dosieroj estas malsamaj$N⏭️: senkondiĉe preterlasi ĉiujn ekzistantajn dosierojn", //m + "ut_ow": "ĉu anstataŭigi dosierojn ĉe la servilo?$N🛡️: neniam (dosiero estos alŝutita kun nova dosiernomo)$N🕒: anstataŭigi, se servila dosiero estas pli malnova ol via$N♻️: ĉiam anstataŭigi, se dosieroj estas malsamaj$N⏭️: senkondiĉe preterlasi ĉiujn ekzistantajn dosierojn", "ut_mt": "daŭri kalkuladon de kontrolsumoj por aliaj dosieroj dum alŝutado$N$Nmalŝaltinda, se via procesoro aŭ disko ne estas sufiĉe rapidaj", "ut_ask": 'peti konfirmon antaŭ komenco de alŝutado">💭', "ut_pot": "plirapidigi alŝutadon por malrapidaj komputiloj$Nper malkomplikado de fasado", @@ -220,14 +220,14 @@ Ls.epo = { "cl_reset": "restarigi", "cl_hpick": "alklaki la kapojn de kolumnoj por kasi en la suban tabelon", "cl_hcancel": "kaŝado de kolumno nuligita", - "cl_rcm": "dekstra-klaka menuo", //m + "cl_rcm": "dekstra-klaka menuo", "ct_grid": '田 krado', "ct_ttips": '◔ ◡ ◔">ℹ️ ŝpruchelpiloj', "ct_thumb": 'dum krado-vido, baskuli montradon de simboloj aŭ bildetoj$NFulmoklavo: T">🖼️ bildetoj', "ct_csel": 'uzi STIR kaj MAJ por elekti dosierojn en krado-vido">elekto', - "ct_dsel": 'uzi tren-elekton en krado-vido">treni', //m - "ct_dl": 'devigi elŝuton (ne montri enkadre) kiam dosiero estas alklakita">dl', //m + "ct_dsel": 'uzi tren-elekton en krado-vido">treni', + "ct_dl": 'devigi elŝuton (ne montri enkadre), kiam dosiero estas alklakita">dl', "ct_ihop": 'montri la lastan viditan bildo-dosieron post fermado de bildo-vidilo">g⮯', "ct_dots": 'montri kaŝitajn dosierojn (se servilo permesas)">kaŝitaj', "ct_qdel": 'peti konfirmon nur unufoje antaŭ forigado">rapid-forig.', @@ -264,8 +264,8 @@ Ls.epo = { "cdt_lim": "maks. nombro de dosieroj por montri en dosierujo", "cdt_ask": "je malsupro de paĝo, peti por ago$Nanstataŭ ŝarĝi pli da dosieroj", "cdt_hsort": "kiom da ordigo-reguloj (<code>,sorthref</code>) inkludi en adreso de la paĝo. Se agordita kiel 0, reguloj, inkluditaj en la adreso, estos ignoritaj", - "cdt_ren": "ebligi propran dekstra-klakan menuon, la normala menuo restas alirebla per shift + dekstra klako", //m - "cdt_rdb": "montri la normalan dekstraklakan menuon kiam la propra jam estas malfermita kaj oni denove dekstre klakas", //m + "cdt_ren": "ebligi propran dekstra-klakan menuon, la normala menuo restas alirebla per MAJ + dekstra klako", + "cdt_rdb": "montri la normalan dekstraklakan menuon, kiam la propra jam estas malfermita kaj oni denove dekstre klakas", "tt_entree": "montri arbovidan navig-panelon$NFulmoklavo: B", "tt_detree": "montri paĝnivelan navig-panelon$NFulmoklavo: B", @@ -459,7 +459,7 @@ Ls.epo = { "tvt_prev": "montri malsekvan dokumenton$NFulmoklavo: i\">⬆ malsekva", "tvt_next": "montri sekvan dokumenton$NFulmoklavo: K\">⬇ sekva", "tvt_sel": "elekti dosieron   ( por eltondado / kopiado / forigado / ... )$NFulmoklavo: S\">elekti", - "tvt_j": "beligi json$NFulmoklavo: shift-J\">j", //m + "tvt_j": "beligi JSONon$NFulmoklavo: MAJ-J\">j", "tvt_edit": "malfermi dosieron en teksto-redaktilo$NFulmoklavo: E\">✏️ redakti", "tvt_tail": "observi ŝanĝojn en dosiero; novaj linioj estos tuje montritaj\">📡 gvati", "tvt_wrap": "linifaldo\">↵", From 81e5eb7b27c6f088d5301880c4fa202583c38be0 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 23 Jan 2026 00:31:13 +0000 Subject: [PATCH 41/54] shares: lan button; closes #1232 --- copyparty/web/shares.html | 6 +++--- copyparty/web/shares.js | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/copyparty/web/shares.html b/copyparty/web/shares.html index 551a3281..7f5c6556 100644 --- a/copyparty/web/shares.html +++ b/copyparty/web/shares.html @@ -6,7 +6,7 @@ {{ s_doctitle }} - + @@ -40,8 +40,8 @@ {%- for k, pw, vp, pr, st, un, t0, t1 in rows %} - qr - {{ k }} + qr + {{ k }} delete {{ "yes" if pw else "--" }} diff --git a/copyparty/web/shares.js b/copyparty/web/shares.js index 3e627446..783b4561 100644 --- a/copyparty/web/shares.js +++ b/copyparty/web/shares.js @@ -63,7 +63,17 @@ function showqr(href) { var buf = []; for (var a = 0; a < tr.length; a++) { - tr[a].cells[0].getElementsByTagName('a')[0].onclick = qr; + var td = tr[a].cells[0], + sa = td.getElementsByTagName('a'), + h0 = sa[0].href, + h1 = sa[1].href; + sa[0].onclick = qr; + if (!h0.startsWith(h1)) { + var a2 = mknod('a', '', sa[1].innerHTML); + a2.href = h0.slice(0, -3); + sa[1].innerHTML = 'LAN'; + td.appendChild(a2); + } for (var b = 7; b < 9; b++) buf.push(parseInt(tr[a].cells[b].innerHTML)); } From b4fddbc3d2f2dcdc33c2809ffb2f1c07f76e034e Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 23 Jan 2026 00:46:05 +0000 Subject: [PATCH 42/54] no keepalive when request is proxied; consider each request individually rather than the previous general approach --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 6917dec0..ad42c249 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -397,9 +397,9 @@ class HttpCli(object): trusted_xff = False n = self.args.rproxy if n: - self.keepalive = False zso = self.headers.get(self.args.xff_hdr) if zso: + self.keepalive = False if n > 0: n -= 1 From c249ee457b575a1a1a0898544afda7f7304ce03a Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 23 Jan 2026 00:55:08 +0000 Subject: [PATCH 43/54] v1.20.4 --- copyparty/__version__.py | 4 ++-- copyparty/web/rups.html | 2 +- docs/changelog.md | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 4c43277a..2fd34538 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 20, 3) +VERSION = (1, 20, 4) CODENAME = "sftp is fine too" -BUILD_DT = (2026, 1, 21) +BUILD_DT = (2026, 1, 23) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/copyparty/web/rups.html b/copyparty/web/rups.html index c657d09b..832dbb50 100644 --- a/copyparty/web/rups.html +++ b/copyparty/web/rups.html @@ -6,7 +6,7 @@ {{ s_doctitle }} - + diff --git a/docs/changelog.md b/docs/changelog.md index 03b5d239..712751d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,24 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2026-0121-0505 `v1.20.3` dillo approves + +## 🧪 new features + +* send-message-to-serverlog now also available as url-parameter `?smsg=foo` 6dcb1efb + * option `smsg` configures which HTTP-methods to allow; can be set to `GET,POST` but default is only `POST` because `GET` is dangerous (CSRF) + +## 🩹 bugfixes + +* #1227 [dillo](https://dillo-browser.github.io/) was not able to login because dillo is more standards-compliant than every other browser (nice) b4df8fa2 +* a web-scraper which got banned for making malicious requests could remain banned for one request longer than intended (wait why did I fix this) ba67b279 +* `?ls` was still a bit jank 0a3a8072 + +## 🌠 fun facts + +* this 6AM release was [powered by void/mournfinale](https://www.youtube.com/watch?v=lFEEXloqk9Q&list=PLlEk36g9RI8Ppjr3HhaO3wjjmA6HnSo2U) +* was going to name the release "dilla på dillo" but somehow google-translate thinks that means "fuck on fuck" which would have been inappropriate + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2026-0119-0126 `v1.20.2` xattrs + range-select From d750f3f0d2a2daf0466f73c4d81dfae6b68f63ef Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 23 Jan 2026 00:59:41 +0000 Subject: [PATCH 44/54] update pkgs to 1.20.4 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/makedeb-mpr/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index 93cd9023..386083d8 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -3,7 +3,7 @@ # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead. pkgname=copyparty -pkgver="1.20.3" +pkgver="1.20.4" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -24,7 +24,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("etc/${pkgname}/copyparty.conf" ) -sha256sums=("894e2857852ddbbc8b9560732be1084f70010cdf1c5aac18ba9f72699f8ec933") +sha256sums=("25dac1edc91c5228d24ef4d07c31c07331cf697951afe31883fba74e2d102cc2") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 6b0f323f..60e10e6a 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.20.3 +pkgver=1.20.4 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("/etc/${pkgname}.d/init" ) -sha256sums=("894e2857852ddbbc8b9560732be1084f70010cdf1c5aac18ba9f72699f8ec933") +sha256sums=("25dac1edc91c5228d24ef4d07c31c07331cf697951afe31883fba74e2d102cc2") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 42b6dacd..2f7159e2 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.20.3/copyparty-1.20.3.tar.gz", - "version": "1.20.3", - "hash": "sha256-iU4oV4Ut27yLlWBzK+EIT3ABDN8cWqwYup9yaZ+OyTM=" + "url": "https://github.com/9001/copyparty/releases/download/v1.20.4/copyparty-1.20.4.tar.gz", + "version": "1.20.4", + "hash": "sha256-JdrB7ckcUijSTvTQfDHAczHPaXlRr+MYg/unTi0QLMI=" } \ No newline at end of file From 59e2d77df93cbaaab3a2b9564f4a5d0496b8ea5a Mon Sep 17 00:00:00 2001 From: George Rawlinson Date: Sun, 25 Jan 2026 06:16:20 +1300 Subject: [PATCH 45/54] update prism webdep (#1238) * update prism to 1.30.0 * split out prism plugins * deps: list all prism langs --- scripts/deps-docker/Dockerfile | 2 +- scripts/deps-docker/genprism.sh | 259 +++++++++++++++++++++++++++++++- 2 files changed, 259 insertions(+), 2 deletions(-) diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile index 6ea7966e..93931ea4 100644 --- a/scripts/deps-docker/Dockerfile +++ b/scripts/deps-docker/Dockerfile @@ -7,7 +7,7 @@ ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \ ver_mde=2.18.0 \ ver_codemirror=5.65.18 \ ver_fontawesome=5.13.0 \ - ver_prism=1.29.0 \ + ver_prism=1.30.0 \ ver_zopfli=1.0.3 # versioncheck: diff --git a/scripts/deps-docker/genprism.sh b/scripts/deps-docker/genprism.sh index 0a9ff227..a0fca276 100755 --- a/scripts/deps-docker/genprism.sh +++ b/scripts/deps-docker/genprism.sh @@ -1,63 +1,320 @@ #!/bin/bash set -e +plugins=( + line-highlight + line-numbers + autolinker +) + langs=( markup css clike javascript + # abap + # abnf + # actionscript + # ada + # agda + # al + # antlr4 + # apacheconf + # apex + # apl + # applescript + # aql + # arduino + # arff + # armasm + # arturo + # asciidoc + # aspnet + # asm6502 + # asmatmel autohotkey + # autoit + # avisynth + # avro-idl + # awk bash basic batch + # bbcode + # bbj + # bicep + # birb + # bison + # bnf + # bqn + # brainfuck + # brightscript + # bro + # bsl c csharp cpp + # cfscript + # chaiscript + # cil + # cilkc + # cilkcpp + # clojure cmake + # cobol + # coffeescript + # concurnas + # csp + # cooklang + # coq + # crystal + # css-extras + # csv + # cue + # cypher + # d + # dart + # dataweave + # dax + # dhall diff + # django + # dns-zone-file docker + # dot + # ebnf + # editorconfig + # eiffel + # ejs elixir + # elm + # etlua + # erb + # erlang + # excel-formula + # fsharp + # factor + # false + # firestore-security-rules + # flow + # fortran + # ftl + # gml + # gap + # gcode + # gdscript + # gedcom + # gettext + # gherkin + # git glsl + # gn + # linker-script go + # go-module + # gradle + # graphql + # groovy + # haml + # handlebars + # haskell + # haxe + # hcl + # hlsl + # hoon + # http + # hpkp + # hsts + # ichigojam + # icon + # icu-message-format + # idris + # ignore + # inform7 ini + # io + # j java + # javadoc + # javadoclike + # javastacktrace + # jexl + # jolie + # jq + # jsdoc + # js-extras json + # json5 + # jsonp + # jsstacktrace + # js-templates + # julia + # keepalived + # keyman kotlin + # kumir + # kusto latex + # latte less + # lilypond + # liquid lisp + # livescript + # llvm + # log + # lolcode lua + # magma makefile + # markdown + # markup-templating + # mata matlab + # maxscript + # mel + # mermaid + # metafont + # mizar + # mongodb + # monkey moonscript + # n1ql + # n4js + # nand2tetris-hdl + # naniscript + # nasm + # neon + # nevod + # nginx nim + # nix + # nsis objectivec + # ocaml + # odin + # opencl + # openqasm + # oz + # parigp + # parser + # pascal + # pascaligo + # psl + # pcaxis + # peoplecode perl + # php + # phpdoc + # php-extras + # plant-uml + # plsql + # powerquery powershell + # processing + # prolog + # promql + # properties + # protobuf + # pug + # puppet + # pure + # purebasic + # purescript python + # qsharp + # q + # qml + # qore r + # racket + # cshtml jsx + # tsx + # reason + # regex + # rego + # renpy + # rescript + # rest + # rip + # roboconf + # robotframework ruby rust + # sas sass scss + # scala + # scheme + # shell-session + # smali + # smalltalk + # smarty + # sml + # solidity + # solution-file + # soy + # sparql + # splunk-spl + # sqf sql + # squirrel + # stan + # stata + # iecst + # stylus + # supercollider swift systemd + # t4-templating + # t4-cs + # t4-vb + # tap + # tcl + # tt2 + # textile toml + # tremor + # turtle + # twig typescript + # typoscript + # unrealscript + # uorazor + # uri + # v + # vala vbnet + # velocity verilog vhdl + # vim + # visual-basic + # warpscript + # wasm + # web-idl + # wgsl + # wiki + # wolfram + # wren + # xeora + # xml-doc + # xojo + # xquery yaml + # yang zig ) slangs="${langs[*]}" slangs="${slangs// /+}" +splugins="${plugins[*]}" +splugins="${splugins// /+}" + for theme in prism-funky prism ; do - u="https://prismjs.com/download.html#themes=$theme&languages=$slangs&plugins=line-highlight+line-numbers+autolinker" + u="https://prismjs.com/download.html#themes=$theme&languages=$slangs&plugins=$splugins" echo "$u" ./genprism.py --dir prism-$1 --js-out prism.js --css-out $theme.css "$u" done From d734786c1d54acc2f7887a0f27a7c918d579ed6d Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 24 Jan 2026 19:37:54 +0000 Subject: [PATCH 46/54] meta: gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 1ea1ff51..530f4727 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ *.png binary *.gif binary +*.gz binary From bef30ac04d6519c0f30fab6c35c1822c20aa7bdb Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 25 Jan 2026 00:22:54 +0000 Subject: [PATCH 47/54] webdeps: vendor asmcrypto.js; npm is no longer able to build it, and the build output never changed since copyparty v0.11.20 (2021-06-20) / asmcrypto 2821dd1dedd1196c378f5854037dda5c869313f3 (2018-12-02) one final pass of reasonable size-golfing was done by hand deflated with pigz -11 -I250 --- copyparty/web/deps/README.md | 18 ++++++++++++++++++ copyparty/web/deps/sha512.ac.js.gz | Bin 0 -> 6667 bytes docs/lics.txt | 2 +- scripts/deps-docker/Dockerfile | 18 +++--------------- scripts/make-pypi-release.sh | 1 + scripts/make-sfx.sh | 2 +- scripts/make-tgz-release.sh | 3 ++- 7 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 copyparty/web/deps/README.md create mode 100644 copyparty/web/deps/sha512.ac.js.gz diff --git a/copyparty/web/deps/README.md b/copyparty/web/deps/README.md new file mode 100644 index 00000000..41a4d853 --- /dev/null +++ b/copyparty/web/deps/README.md @@ -0,0 +1,18 @@ +this folder *mostly* contains third-party dependencies; run `make -C scripts/deps-docker` to build the following files and have them appear here: + +* `easymde.css.gz` and `easymde.js.gz` is the fancy markdown editor, [EasyMDE](https://github.com/Ionaru/easy-markdown-editor) +* `marked.js.gz` is the markdown rendering library [Marked](https://github.com/markedjs/marked) +* `mini-fa.css.gz` and `mini-fa.woff` is a small subset of [fontawesome](https://github.com/FortAwesome/Font-Awesome) +* `prism.css.gz` and `prism.js.gz` is the syntax highlighter [PrismJS](https://prismjs.com/) +* `scp.woff2` is a subset of the monospace font [Source Code Pro](https://github.com/adobe-fonts/source-code-pro) +* `sha512.hw.js.gz` is the Wasm sha512 library [hash-wasm](https://github.com/Daninet/hash-wasm) + +additionally, the following files are vendored into the copyparty git repository, but do NOT originate from the copyparty project (as mentioned in `--license`): + +* `sha512.ac.js.gz` is a compiled and slightly golfed/modified [asmcrypto.js](https://github.com/asmcrypto/asmcrypto.js), © 2013 Artem S Vybornov (MIT-Licensed) + * vendored because it no longer builds with modern versions of NodeJS/npm + * is only loaded by *really old* webbrowsers (ie11, firefox 51, chrome 56) + +finally, there is also the following files which *does* originate from the copyparty project, yet appear here for technical reasons: + +* `busy.mp3.gz` is a short mp3-file to make iphones stop glitching out diff --git a/copyparty/web/deps/sha512.ac.js.gz b/copyparty/web/deps/sha512.ac.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..ce98a7fe3a1bb98959f96ff6214947a92f65c2fa GIT binary patch literal 6667 zcmY*)Wmr^UyEQEW5(-F%bPXU<(v5V3(ntvm9ZH8VlynLVp_FuubT~8!NW)M=IMmRc zXLvo=ci!(>fA(JMj%TmuzOPLmgNsZ4((3~TriZguzNtsP#0?pZIL!a-PyC_AHQ&YD@-5QSZXw_m!N!{BYjONsA`0iJZ0>*zIP!q+nJgi; zJ~pWyPkx>QHnzSad&#`C%oI7^4{t5422dz6yKBsoP;LU7K%Ljp-WOYL!jS?m=o6ig zyTlM-iYy1Khddx%cIyvD;!vTMZ;Ei|h7P+#v+TM%M+)7>7uP0ImG~puMiab3rB8Au zfbj8eV*lbLUZkt$XBVxw;w4*#u0&vRszp$SQWKW)QQG1kKp6T;P&$9QGPUk0dlV7%==p}nQDW}G-bp+*TrS3Ia7&8Kv1zv%>FR)!D- zlBMyELD`@rlCMVmW}eqMEYfNd+P_Xuz52-wIULz;W%{Al*7tfZ%8X(^m62@U*J9pz zhbb*Es2h)Nl=alrD>OFaT{_o@j6^MF8>v4Q>=|EsS7&~+r5u0f$&3t(k#wJX^gLj_ zmgukKQNgHjP{!^bDa~nB5uCsa-}!->^m}nDq6*OT+U(lBS7^5x;T4)+5E`v%{Oski z{nqR5X?o3h`?Wt*bAWV!CWUVxLU+IlzL0kGsD}~Uzn96 zE~ZCqFjE5G?8)pSCdZp%Ogy_56k69`+=*R#$Cg`S$yt6>O~S3E3AyEk6PmmIzVQ3~ zp)HkP6H$Gai`X&fSUSt)8OnHLZ2o%7I_r<fJV0(_^PDawqc|S{`pVuqs=wlc{mW zQoC!NO*OW0bwCe9t7dO0O+$2!&;x1a1ho~hvZ@C@6qsY}_LVnh%i58<&h-l_e1Z#Nfe*2OIc|rS zaOYfF8|QqDt3>g~|9E2-<&}O_JJGqbr|uO7-i>o7c7rK)vEW^8f~QdhI-%P`wU)h1EXg)la%f%G1g?>-BXUhF*1 zk)ul9&0hu3AwdKF(J4xBFXSrqiO20T&0Z%*$+fv;7jHfU)|Us}JXMAZj9Ip~V|LMR zCLjuLvNjUz%r1qk{gPqSJ6r0Z!thH0_(z0eb4&49*`)5~)b#bfLi3%xXYKK^MZ097 z`#tCm{_D15BlqNZ`|=*xwA2tp0bib*4Unu#smRvvpH2hb1H8dUB&|*@7as!dpZXo! z#Mva$U$LC6_#H@Gqzw_3UT7Z>w}CkV3B}j1{TQU}WL#B4jfoeMR>-k_a*L>aZ2#Nh zhu5Ye5l)Ba{Kj>QOgv#F1e=KAqZYY%-U>R_zGyC1q0>gWY#KHZbK}Y5z1n5qESOh? z`9jh~a^pfyYLGsZs0*}mb_6xJ5%D*AXhkl$i}11hJQA`Jhvmc2$kauPyJzH@ap3>k z=bY3x)fZ2KbRlsCqU#zKOnn4<)&67$h~|RneV#qcj>CoBfIR?|bAm}ij#7qv&be2t$ z&1JrrC=M(P*#+s?JRtiiOIS4{&=ZgVWrFRg{zP+nWF>|M3z;SC^tOOl{yG+1Uew*x z-{v?;KuSAZYe0Bvfl)x%EMyph4dJO?^mtiDQBFEZ&J5wHL0-=i_Kfrv9W*ERR2tW) zE(C3Aq;;KH_*K&aUd#5R<}q!as<}~a;`T%C$9XVdiL-H?-pmlDYT8WTe|u*w_7s+% z3GkJbQlIoi``;BY{1h)fg7~?}4bziA2s0K$IX7HAre0c*A!h3N9Z%IlX?!Q~ z*VRm7`&mZ#^YC|GGD}t7coQQHS}JwtshQn9#`^tqwC| z(W5r+pL=zfnK@U(BO1(&qOE{+6*pedstF>~e!*cnTcWyS1*OXBGcW5lIu34s+DpeZ z{103f-i&ql8K(Wa7`D%asC&iCCl)2b%?iP?M85&n-g~da3g!}r<9_e<+bZ#ih0aN^ z#YA_h$N*(tiY3i8jqULSqu@`9@-3R0I&|KI- z`%IP)e=vkX$C#j+Ct`sd(>2Dj$+G`ga7|ag+@O-vK--1C+h=QCjBz>YLgUMRw?Es8 z82YkApX$GMt70U}h4C&eBTH}jtBfy;cCDk_Xn!8v5-*4Ccm2#5BIGBV?f}f}f$+-A zK@?_pS9~U8u9i^jC(ngY4~)=H?V@>Y`$QgpkKL!0E8KJEctga_5w?) zNxdV--pxMWB$xR~(5x`V6Xs0fshW5E9YG%^Y18ArQEQbcjZ!>A&4^EA^U`sAEywVK z&2UfJ1;~5s3risEA!ZbwTbKb$kVU7gpk=t|e*X7>HyvIe1X@6_r1Vc=M#!(hS?q;d zElbl~;_C)J#XK>rMgC1f86SdXxiQ+1kz{ZX!Bq;`Q0h>$4F*bqJoDsdZ>D%*swQAc z@C|^Y_gt>r2RzAM$W=@>F5f}{Sv;HDo8~|S)a)CZr8iOv?Cy~OH1_U1l2!-I!Eq&6_ zRx#Az`ab7v2{X+__GErg5o+B>H-n}`U%c8wr$aRg8uAVjTjko#HJx50BJ&<4JT!;d z$(u>TFky6K4|`KDk$@E6y6nr-d|PAERBptIQuh3SKdoDAU_7(VbKW)fEi5oGO^4L+ z)Cs%~ki}YLyT@7&MLKX{Sy-XZCK}M+$nHBPX@Ac%5B;rNQeEV&?IWdUq%Fqm&t01M z5yx5>s|9`LNsY6#mt4tlEP|eCNj==Qut@fqjPUNK&C`WMM4W`rO4Iw%o_s`{_`cY8 zkwVXndfBfMJ}CM4CTMrz56Nc|g5a(++|TYwdG}8%(F}uw{yJLG&y4RGPb`8EoVvD# zSFYlH9#^hc2g26}^XdJfZl~~q$^k{cdLod^MG;Xk3NDNiMJHuM={ctEl)R^K+_;q! z_yc|yL?gXU;a+b}yjBaPXdsqZQrRf@SI)JTd)&e+QW6aSytnY`C$Jm%1E9#0+Ot#8 zd77GP*?;padZtb$L5*k50}JL?P)lmaCG-Jhdp3UOJ4E!N%tgC)cdoM$z}de<2%=Fh zw)DT>&1U`Y!#vvaJFyDb1@BFsa2)a6uwAb`INmZlj)&ymhXVWBT6)ygE~BiMim zap%B5xO||_pn(F|QOIQjulpD<&m=(>$s`i`Eb;+W0%vEURnjK)gi}{~41}=if6?v7 zC$F7QRZ79)t!<={P$8#(;Yu-c<>_3+yaGyiyu8?}s0+bbeL%~kIQI=+_l_B~d|ov~ z#0e@CB7#Dr18<9c6UO0RfnVF7ScySEl=T>H7>84){zfQT()!$EP#{Fa_PvnpL;ar( z@k3Eu#51o)657lOy7{T=9(O;$ae%Y97{yywqzB@5!>QXJNGak-W61%N4Z?1vFZoP{ z7;e&!JRfRnVy({k@_6(~M8@N)C*g#N4~3o$QG1i0``@+GA9>R?ive zOzYy9TnJZ#2{#E=9{~G)F1|+94V52?yWM zu(dm=>M&A_GDsvZ2*dNv6@>#z(L(+E8+gO|l;HNvoh#}>{_zL3Wgk2452*eaoZdD_ zJsA9-L*B@{H23UDwW>?JwDU(2p$vXlGXxPYnv&MZiR!q-QW?@GY0XV61z1410l5rf!pijm%rE zJy#M1QZ7|apNevJuEA%3f=DYH^^MZhCpL1QpbDhgh<(EJEXF_ zs^*0#dMy2C^gpWQg)N=?yQo|M6dK#_>o{y&qkY~I zLcD!}61GhP@S&iuNw(ib?X~NffiEh)bT$~YPCs@EEl9H0_aw#aVRqxR>SM@>(Mkg7 z>ybWDVB`Oh!EQyF|59fNH;qI_`%OLP>*x@xB%>krG;Qv0dWwb7(FOUN%!*{s6u95( z^*T^OU$U}uMj0q5@oT9)b)@Wj8E2C{*r=zi{facFyTqy>(k66Jp68hnE0zMwpC6m- zIMHvcc#@4+Yt(cyQ#d)JBMOo!^wmgfhUIOF-r4?onVeU`{Em#aN1Fdlxy^j!hxth0 zyt3OoJGK5>K?B;U*^+_n&z$igNGf~UDb{k%9}w-FDGNdE#mQ}xGLMS5hGr){n<*nN zy*0jts`n=oQ~~DTv3=FT>(F-)UH2Vsy!O1WwS1I8*&|IF z?xTy%oSbP5t}K?8kx5^+-X1q6?DQ9@1St(mndRYM`PW4nzOq~r8r*Biy?&k|D_zL( zd7QIlziB^5NCJcNlvTac4AXQWFONHky_ApdLT zD|98sU>Az|-?E_%9EJ0oSpArd$V4sRs}C4wvS_fCdLY5$X%m$OcC3s?_AsNT<%KB@ zn(!w}LRi3KRDRZf^6B${bWh9GIi?I4rJXY#@_D|#Lib|~rp*KLGaGquOcfAYI2+aT zq+vOtn&^InI-!c~KK-Gd%(}vU6L!xlu$+4>eAylph~Tffz0qOLRV3(?CPr&2)4t)W z{LAEiKl{3|&RQ-evSYLZu|$_I2WDH!nNcmDb$CpgF7PeYDjTfwngy6VeN55e|Ga0{?$>BqjsxKuZ1jXV<8a1NUL*X_ z!=qwgG*{}mcW@UUvApF4CpKx1a}slj0y=xRNuKmCd&cvbptDEOTY+?*%UCa9j(*~* zJ4(PH`s%n*H?#BFWq{w~K!+bi5t9j2y6+uS)JsJLi_<2P-sy6^?m&n|+v$PPgT^S# zBrG~Nt&d{~><8N^DaTTH1YYKGwAffn>k19_2N&%oCZem)(J^73O=A3pqUnciQ>Ucv zF-W8Bpbepwp~av%qN$>Bq2Z%d5w(gRYwLzxZ z&X;(kB24TL*;ky->&^*2Com4I1)#q{1f5B1xXLJMiVpRz+d2{~HL+F>=@*o#9O;ig z4>!Bhx04HMvW{MTPG_Q6*k8ao!9lJg>0%u@gKeVRrd_(D-bDH+&&@M+BRp;Vz}JPd zQyi1$wj)#Hi!GCLZXPW@75{1;%PRxrKpznZutzlz^NWnKV%8z>ch+( z+RW}0#T1c`H24r>GWw8bs^%Fj=M((M#gQC_>u)!u%1nnHiu9EqqB>(H=qhswpY_8F z_!yIW{WLj<|Mnx<;=95w1zyFymx?g}O2w#A55GvJ8!9-)kA(-VWJ^)u4Ie(nvTf(I zapqoT<9PX~lK@kgOnPu@(nB7>ev12@Fr_I9M{DsO`#>*HZ*A)vRdHKx#qZX>DkOgp zt!cO0BRBuopGCG9MnGuAIDA=&Xl}go-?PP+6>|B%mWHwBPLJy2^^_|DK?dWu-kOeW{v+wbYQyxhVTe00=_73}xRKaO;MG$nYv zw9eg&CGASZUn@<^^*s~3q3#>vIdADVevx>)@6R$xuI1XxbK7*&{(R}?saRSZMl>^< zf@enFtp74XUij$$Z4e@?G)(0kFNs?ja4fm(T_i}0zIi1Ob|6k(l@N)`&qPP?xSh5B zeG6vmMgBqFd^btvu*w0)*_((yMWeDq;tEzCdbz0>1{ zbSR!Qt|ez#es*??TEkf${=8)&qyZ_#T6;lXxGPeXsP^fvYz7^M2xbRR>?`i(ole)++muk>5G0}PDlf+|0r!!XxLbn4l%b(&wPnT7m13m??=A2a6bv@(b22(v1!Hbl;32 zSjvTn%_3EcyP4}QLZcQh_?IIetQhsC-=K8%Zr3Kj~%RZQeF? zQ_T92zq6HJ>GpQprTXMLL#dcN^3ExM6Jon`P!a1ar|5povIOx_45^OV$g2^pX}7pl zzb08FIhFtHnl0whuk6LqIUO}rG+2%sZ`&Ye`FK6-izC-iM75u-M5nRCeMDDR9z>wQ zu|1uD-yFY+^4K)dd4uY$TK8qF2bI`c^D09dQ*1Tu8;8RuWR!al+ src/entry-export_all.ts \ - && node -r esm build.js \ - && awk '/HMAC state/{o=1} /var HEAP/{o=0} /function hmac_reset/{o=1} /return \{/{o=0} /var __extends =/{o=1} /var Hash =/{o=0} /hmac_|pbkdf2_/{next} o{next} {gsub(/IllegalStateError/,"Exception")} {sub(/^ +/,"");sub(/^\/\/ .*/,"");sub(/;$/," ;")} 1' < asmcrypto.all.es5.js > /z/dist/sha512.ac.js - - # build hash-wasm RUN cd hash-wasm/dist \ && mv sha512.umd.min.js /z/dist/sha512.hw.js diff --git a/scripts/make-pypi-release.sh b/scripts/make-pypi-release.sh index aecfea94..7ed3b8a0 100755 --- a/scripts/make-pypi-release.sh +++ b/scripts/make-pypi-release.sh @@ -151,6 +151,7 @@ done rm -rf contrib [ $fast ] && sed -ri s/573/10/ copyparty/web/Makefile (cd copyparty/web && make -j$(nproc) && rm Makefile) +rm -f copyparty/web/deps/README.md # build python3 -m build diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index 770080d3..afd12c3b 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -435,7 +435,7 @@ find -name py.typed -delete find -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -fv -- "$f"; done -rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile +rm -f copyparty/web/deps/*.full.* copyparty/web/deps/README.md copyparty/web/dbg-* copyparty/web/Makefile find copyparty | LC_ALL=C sort | sed -r 's/\.gz$//;s/$/,/' > have cat have | while IFS= read -r x; do diff --git a/scripts/make-tgz-release.sh b/scripts/make-tgz-release.sh index c68766f9..f7cf25ca 100755 --- a/scripts/make-tgz-release.sh +++ b/scripts/make-tgz-release.sh @@ -91,7 +91,8 @@ grep -qE "^VERSION *= \(${commaver}\)$" copyparty/__version__.py || } rm -rf .vscode -rm \ +rm -f \ + copyparty/web/deps/README.md \ .gitattributes \ .gitignore From 5c4ba376a0356637598397272959d37e6a96e7fb Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 25 Jan 2026 00:25:24 +0000 Subject: [PATCH 48/54] fix ie11 spinlock in write-only folders --- copyparty/httpcli.py | 8 ++++++++ copyparty/web/browser.js | 5 +++-- copyparty/web/util.js | 3 ++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index ad42c249..8e26701c 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6978,6 +6978,14 @@ class HttpCli(object): if "zip" in self.uparam or "tar" in self.uparam: raise Pebkac(403) + zsl = j2a["files"] = [] + if is_js: + j2a["ls0"] = cgv["ls0"] = { + "dirs": zsl, + "files": zsl, + "taglist": zsl, + } + html = self.j2s(tpl, **j2a) self.reply(html.encode("utf-8", "replace")) return True diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 0bbd32b1..5cb8a943 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -7560,9 +7560,10 @@ var treectl = (function () { qsr('#bbsw'); srvinf = ebi('srv_info').innerHTML.slice(6, -7); if (ls0 === null) { - setck('js=y'); r.ls_cb = showfile.addlinks; - return r.reqls(get_evpath(), false, undefined, true); + return setck('js=y', function () { + r.reqls(get_evpath(), false, undefined, true); + }); } ls0.unlist = unlist0; ls0.u2ts = u2ts; diff --git a/copyparty/web/util.js b/copyparty/web/util.js index ec896d74..357f8e5c 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -1326,9 +1326,10 @@ function scfg_bind(obj, oname, cname, defval, cb) { return v; } -function setck(v) { +function setck(v, cb) { var xhr = new XHR(); xhr.open('GET', SR + '/?setck=' + v, true); + xhr.onload = cb; xhr.send(); } From 69fa1d10bc0f28d56f7fc1622640b1b292b5a8d5 Mon Sep 17 00:00:00 2001 From: 000yesnt <49329895+000yesnt@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:25:30 -0300 Subject: [PATCH 49/54] update Portuguese translation (#1245) * portuguese tl: verify and adjust strings * portuguese tl: fix newlines * portuguese tl: change rc_zip wording --- copyparty/web/tl/por.js | 106 ++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/copyparty/web/tl/por.js b/copyparty/web/tl/por.js index 29aea56c..6fd11845 100644 --- a/copyparty/web/tl/por.js +++ b/copyparty/web/tl/por.js @@ -84,8 +84,8 @@ Ls.por = { ["M", "fechar arquivo de texto"], ["E", "editar arquivo de texto"], ["S", "selecionar arquivo (para recortar/copiar/renomear)"], - ["Y", "baixar arquivo de texto"], //m - ["⇧ J", "embelezar json"], //m + ["Y", "baixar arquivo de texto"], + ["⇧ J", "embelezar json"], ] ], @@ -118,7 +118,7 @@ Ls.por = { "ot_unpost": "despublicar: excluir seus uploads recentes, ou abortar os que não foram concluídos", "ot_bup": "bup: uploader básico, até suporta netscape 4.0", "ot_mkdir": "mkdir: criar um novo diretório", - "ot_md": "new-file: criar um novo ficheiro de texto", //m + "ot_md": "new-file: criar um novo arquivo de texto", "ot_msg": "msg: enviar uma mensagem para o log do servidor", "ot_mp": "opções do reprodutor de mídia", "ot_cfg": "opções de configuração", @@ -127,7 +127,7 @@ Ls.por = { "ot_noie": 'Por favor, use Chrome / Firefox / Edge', "ab_mkdir": "criar diretório", - "ab_mkdoc": "novo ficheiro de texto", //m + "ab_mkdoc": "novo arquivo de texto", "ab_msg": "enviar msg para o log do srv", "ay_path": "pular para pastas", @@ -155,12 +155,12 @@ Ls.por = { "ul_par": "uploads paralelos:", "ut_rand": "randomizar nomes de arquivos", "ut_u2ts": "copiar o carimbo de data/hora de última modificação$Ndo seu sistema de arquivos para o servidor\">📅", - "ut_ow": "substituir arquivos existentes no servidor?$N🛡️: nunca (irá gerar um novo nome de arquivo em vez disso)$N🕒: substituir se o arquivo no servidor for mais antigo que o seu$N♻️: sempre substituir se os arquivos forem diferentes$N⏭️: ignorar incondicionalmente todos os arquivos existentes", //m + "ut_ow": "substituir arquivos existentes no servidor?$N🛡️: nunca (irá gerar um novo nome de arquivo em vez disso)$N🕒: substituir se o arquivo no servidor for mais antigo que o seu$N♻️: sempre substituir se os arquivos forem diferentes$N⏭️: ignorar incondicionalmente todos os arquivos existentes", "ut_mt": "continuar a fazer o hash de outros arquivos enquanto faz upload$N$Ntalvez desativar se sua CPU ou HDD for um gargalo", "ut_ask": 'pedir confirmação antes do upload começar">💭', "ut_pot": "melhorar a velocidade de upload em dispositivos lentos$Ntornando a UI menos complexa", "ut_srch": "não fazer upload, em vez disso verificar se os arquivos já$N existem no servidor (irá escanear todas as pastas que você pode ler)", - "ut_par": "pausar uploads definindo para 0$N$naumentar se sua conexão for lenta / alta latência$N$nmanter em 1 em LAN ou se o HDD do servidor for um gargalo", + "ut_par": "pausar uploads definindo para 0$N$Naumentar se sua conexão for lenta / alta latência$N$Nmanter em 1 em LAN ou se o HDD do servidor for um gargalo", "ul_btn": "soltar arquivos / pastas
aqui (ou clique em mim)", "ul_btnu": "U P L O A D", "ul_btns": "B U S C A R", @@ -206,7 +206,7 @@ Ls.por = { "u_nav_b": 'ArquivosUma pasta', "cl_opts": "interruptores", - "cl_hfsz": "tamanho do arquivo", //m + "cl_hfsz": "tamanho do arquivo", "cl_themes": "tema", "cl_langs": "idioma", "cl_ziptype": "download de pasta", @@ -220,14 +220,14 @@ Ls.por = { "cl_reset": "resetar", "cl_hpick": "toque nos cabeçalhos das colunas para ocultá-los na tabela abaixo", "cl_hcancel": "ocultar coluna abortado", - "cl_rcm": "menu de clique direito", //m + "cl_rcm": "menu de clique direito", "ct_grid": '田 a grade', "ct_ttips": '◔ ◡ ◔">ℹ️ dicas de ferramentas', "ct_thumb": 'na visualização de grade, alternar entre ícones ou miniaturas$NHotkey: T">🖼️ miniaturas', "ct_csel": 'usar CTRL e SHIFT para seleção de arquivo na visualização de grade">sel', - "ct_dsel": 'usar seleção por arrasto na visualização de grade">arrastar', //m - "ct_dl": 'forçar download (não exibir inline) ao clicar em um arquivo">dl', //m + "ct_dsel": 'usar seleção por arrasto na visualização de grade">arrastar', + "ct_dl": 'forçar download (não exibir na página) ao clicar em um arquivo">dl', "ct_ihop": 'quando o visualizador de imagens for fechado, rolar para o último arquivo visualizado">g⮯', "ct_dots": 'mostrar arquivos ocultos (se o servidor permitir)">dotfiles', "ct_qdel": 'ao excluir arquivos, pedir confirmação apenas uma vez">qdel', @@ -242,18 +242,18 @@ Ls.por = { "cut_turbo": "o botão yolo, você provavelmente NÃO quer habilitar isso:$N$Nuse isto se você estava fazendo upload de uma enorme quantidade de arquivos e teve que reiniciar por algum motivo, e quer continuar o upload o mais rápido possível$N$Nisto substitui a verificação de hash por uma simples "este arquivo tem o mesmo tamanho no servidor?" então se o conteúdo do arquivo for diferente ele NÃO será enviado$N$Nvocê deve desativar isso quando o upload estiver concluído, e então "enviar" os mesmos arquivos novamente para permitir que o cliente os verifique\">turbo", - "cut_datechk": "não tem efeito a menos que o botão turbo esteja ativado$N$Nreduz o fator yolo por uma pequena quantidade; verifica se os carimbos de data/hora dos arquivos no servidor correspondem aos seus$N$ndeve teoricamente pegar a maioria dos uploads incompletos / corrompidos, mas não é um substituto para fazer uma verificação com o turbo desativado depois\">date-chk", + "cut_datechk": "não tem efeito a menos que o botão turbo esteja ativado$N$Nreduz o fator yolo por uma pequena quantidade; verifica se os carimbos de data/hora dos arquivos no servidor correspondem aos seus$N$Ndeve teoricamente pegar a maioria dos uploads incompletos / corrompidos, mas não é um substituto para fazer uma verificação com o turbo desativado depois\">date-chk", "cut_u2sz": "tamanho (em MiB) de cada bloco de upload; valores grandes voam melhor pelo atlântico. Tente valores baixos em conexões muito não confiáveis", "cut_flag": "garantir que apenas uma aba esteja fazendo upload por vez $N -- outras abas devem ter isso ativado também $N -- só afeta abas no mesmo domínio", - "cut_az": "enviar arquivos em ordem alfabética, em vez de o menor primeiro$N$na ordem alfabética pode tornar mais fácil de verificar se algo deu errado no servidor, mas torna o upload um pouco mais lento em fibra / LAN", + "cut_az": "enviar arquivos em ordem alfabética, em vez de o menor primeiro$N$Na ordem alfabética pode tornar mais fácil de verificar se algo deu errado no servidor, mas torna o upload um pouco mais lento em fibra / LAN", "cut_nag": "notificação do SO quando o upload for concluído$N(somente se o navegador ou aba não estiver ativo)", "cut_sfx": "alerta audível quando o upload for concluído$N(somente se o navegador ou aba não estiver ativo)", - "cut_mt": "usar multithreading para acelerar o hash de arquivos$N$nisto usa web-workers e requer$Nmais RAM (até 512 MiB extras)$N$ntorna https 30% mais rápido, http 4.5x mais rápido\">mt", + "cut_mt": "usar multithreading para acelerar o hash de arquivos$N$Nisto usa web-workers e requer$Nmais RAM (até 512 MiB extras)$N$Ntorna https 30% mais rápido, http 4.5x mais rápido\">mt", "cut_wasm": "usar wasm em vez do hasher embutido do navegador; melhora a velocidade em navegadores baseados em chrome mas aumenta a carga da CPU, e muitas versões antigas do chrome têm bugs que fazem o navegador consumir toda a RAM e travar se isso for ativado\">wasm", @@ -262,10 +262,10 @@ Ls.por = { "cft_bg": "cor do fundo", "cdt_lim": "número máximo de arquivos para mostrar em uma pasta", - "cdt_ask": "ao rolar para o final,$nem vez de carregar mais arquivos,$nperguntar o que fazer", + "cdt_ask": "ao rolar para o final,$Nem vez de carregar mais arquivos,$Nperguntar o que fazer", "cdt_hsort": "quantas regras de ordenação (<code>,sorthref</code>) incluir em URLs de mídia. Definir isso para 0 também ignorará as regras de ordenação incluídas em links de mídia quando você clicar neles", - "cdt_ren": "ativar menu de clique direito personalizado, o menu normal permanece acessível com shift + clique direito", //m - "cdt_rdb": "mostrar o menu padrão do botão direito quando o personalizado já estiver aberto e houver novo clique", //m + "cdt_ren": "ativar menu de clique direito personalizado, o menu normal permanece acessível com shift + clique direito", + "cdt_rdb": "mostrar o menu padrão do botão direito quando o menu personalizado já estiver aberto e houver um novo clique", "tt_entree": "mostrar painel de navegação (árvore de diretórios)$NHotkey: B", "tt_detree": "mostrar breadcrumbs$NHotkey: B", @@ -287,7 +287,7 @@ Ls.por = { "mt_loop": "loop/repetir uma música\">🔁", "mt_one": "parar depois de uma música\">1️⃣", "mt_shuf": "embaralhar as músicas em cada pasta\">🔀", - "mt_aplay": "reproduzir automaticamente se houver um ID de música no link que você clicou para acessar o servidor$N$ndesativar isso também impedirá que a URL da página seja atualizada com IDs de música ao tocar música, para evitar a reprodução automática se essas configurações forem perdidas mas a URL permanecer\">a▶", + "mt_aplay": "reproduzir automaticamente se houver um ID de música no link que você clicou para acessar o servidor$N$Ndesativar isso também impedirá que a URL da página seja atualizada com IDs de música ao tocar música, para evitar a reprodução automática se essas configurações forem perdidas mas a URL permanecer\">a▶", "mt_preload": "começar a carregar a próxima música perto do final para uma reprodução sem interrupções\">preload", "mt_prescan": "ir para a próxima pasta antes que a última música$Ntermine, mantendo o navegador feliz$Npara que ele não pare a reprodução\">nav", "mt_fullpre": "tentar pré-carregar a música inteira;$N✅ ativar em conexões não confiáveis,$N❌ desativar em conexões lentas provavelmente\">full", @@ -296,7 +296,7 @@ Ls.por = { "mt_npclip": "mostrar botões para copiar a música que está tocando para a área de transferência\">/np", "mt_m3u_c": "mostrar botões para copiar as$Nmúsicas selecionadas como entradas de playlist m3u8 para a área de transferência\">📻", "mt_octl": "integração com o SO (atalhos de mídia / osd)\">os-ctl", - "mt_oseek": "permitir busca através da integração com o SO$N$nnota: em alguns dispositivos (iPhones),$nisto substitui o botão de próxima música\">seek", + "mt_oseek": "permitir busca através da integração com o SO$N$Nnota: em alguns dispositivos (iPhones),$Nisto substitui o botão de próxima música\">seek", "mt_oscv": "mostrar capa do álbum no osd\">art", "mt_follow": "manter a faixa que está tocando rolando à vista\">🎯", "mt_compact": "controles compactos\">⟎", @@ -318,8 +318,8 @@ Ls.por = { "mt_c2ng": "seu dispositivo não parece suportar este formato de saída, mas vamos tentar mesmo assim", "mt_xowa": "existem bugs no iOS que impedem a reprodução em segundo plano usando este formato; por favor, use caf ou mp3 em vez disso", "mt_tint": "nível de fundo (0-100) na barra de busca$Npara tornar o buffer menos distrativo", - "mt_eq": "ativa o equalizador e o controle de ganho;$N$nimpulsão <code>0</code> = volume padrão de 100% (não modificado)$N$nlargura <code>1  </code> = estéreo padrão (não modificado)$nlargura <code>0.5</code> = 50% de crossfeed esquerda-direita$nlargura <code>0  </code> = mono$N$nimpulsão <code>-0.8</code> & largura <code>10</code> = remoção de vocal :^)$N$natvar o equalizador torna os álbuns sem interrupções totalmente sem interrupções, então deixe-o ligado com todos os valores em zero (exceto largura = 1) se você se importa com isso", - "mt_drc": "ativa o compressor de faixa dinâmica (nivelador de volume / brickwaller); também ativará o EQ para equilibrar o spaghetti, então defina todos os campos EQ exceto 'width' para 0 se você não quiser$N$nabaixa o volume do áudio acima do THRESHOLD dB; para cada RATIO dB após o THRESHOLD há 1 dB de saída, então os valores padrão de tresh -24 e ratio 12 significam que nunca deve ficar mais alto que -22 dB e é seguro aumentar o impulso do equalizador para 0.8, ou até 1.8 com ATK 0 e um enorme RLS como 90 (só funciona no firefox; RLS é no máximo 1 em outros navegadores)$N$n(veja a wikipedia, eles explicam muito melhor)", + "mt_eq": "ativa o equalizador e o controle de ganho;$N$Nimpulsão <code>0</code> = volume padrão de 100% (não modificado)$N$Nlargura <code>1  </code> = estéreo padrão (não modificado)$Nlargura <code>0.5</code> = 50% de crossfeed esquerda-direita$Nlargura <code>0  </code> = mono$N$Nimpulsão <code>-0.8</code> & largura <code>10</code> = remoção de vocal :^)$N$Natvar o equalizador torna os álbuns sem interrupções totalmente sem interrupções, então deixe-o ligado com todos os valores em zero (exceto largura = 1) se você se importa com isso", + "mt_drc": "ativa o compressor de faixa dinâmica (nivelador de volume / brickwaller); também ativará o EQ para equilibrar o spaghetti, então defina todos os campos EQ exceto 'width' para 0 se você não quiser$N$Nabaixa o volume do áudio acima do THRESHOLD dB; para cada RATIO dB após o THRESHOLD há 1 dB de saída, então os valores padrão de tresh -24 e ratio 12 significam que nunca deve ficar mais alto que -22 dB e é seguro aumentar o impulso do equalizador para 0.8, ou até 1.8 com ATK 0 e um enorme RLS como 90 (só funciona no firefox; RLS é no máximo 1 em outros navegadores)$N$N(veja a wikipedia, eles explicam muito melhor)", "mb_play": "reproduzir", "mm_hashplay": "reproduzir este arquivo de áudio?", @@ -336,7 +336,7 @@ Ls.por = { "mm_eunk": "Erro Desconhecido", "mm_e404": "Não foi possível reproduzir áudio; erro 404: Arquivo não encontrado.", "mm_e403": "Não foi possível reproduzir áudio; erro 403: Acesso negado.\n\nTente pressionar F5 para recarregar, talvez você tenha saído da conta", - "mm_e415": "Não foi possível reproduzir áudio; erro 415: Falha na conversão do ficheiro; verifique os logs do servidor.", //m + "mm_e415": "Não foi possível reproduzir áudio; erro 415: Falha na conversão do arquivo; verifique os logs do servidor.", "mm_e500": "Não foi possível reproduzir áudio; erro 500: Verifique os logs do servidor.", "mm_e5xx": "Não foi possível reproduzir áudio; erro do servidor ", "mm_nof": "não encontrando mais arquivos de áudio por perto", @@ -356,7 +356,7 @@ Ls.por = { "f_anota": "apenas {0} dos {1} itens foram selecionados;\npara selecionar a pasta inteira, primeiro role para o final", "f_dls": 'os links de arquivo na pasta atual foram\nalterados para links de download', - "f_dl_nd": 'a ignorar pasta (use o download zip/tar em vez disso):\n', //m + "f_dl_nd": 'pulando pasta (use o download zip/tar em vez disso):\n', "f_partial": "Para baixar com segurança um arquivo que está sendo enviado, por favor, clique no arquivo que tem o mesmo nome, mas sem a extensão .PARTIAL. Por favor, pressione CANCELAR ou Escape para fazer isso.\n\nPressionar OK / Enter irá ignorar este aviso e continuar baixando o arquivo temporário .PARTIAL, o que quase certamente lhe dará dados corrompidos.", @@ -425,10 +425,10 @@ Ls.por = { "fcc_warn": 'copiado {0} itens para a área de transferência\n\nmas: apenas esta aba do navegador pode colá-los\n(já que a seleção é tão absolutamente massiva)', "fp_apply": "usar estes nomes", - "fp_skip": "pular conflitos", //m + "fp_skip": "pular conflitos", "fp_ecut": "primeiro recorte ou copie alguns arquivos / pastas para colar / mover\n\nnota: você pode recortar / colar entre abas diferentes do navegador", - "fp_ename": "{0} itens não podem ser movidos para cá porque os nomes já estão em uso. Dê a eles novos nomes abaixo para continuar, ou deixe o nome em branco (\"pular conflitos\") para pular:", //m - "fcp_ename": "{0} itens não podem ser copiados para cá porque os nomes já estão em uso. Dê a eles novos nomes abaixo para continuar, ou deixe o nome em branco (\"pular conflitos\") para pular:", //m + "fp_ename": "{0} itens não podem ser movidos para cá porque os nomes já estão em uso. Dê a eles novos nomes abaixo para continuar, ou deixe o nome em branco (\"pular conflitos\") para pular:", + "fcp_ename": "{0} itens não podem ser copiados para cá porque os nomes já estão em uso. Dê a eles novos nomes abaixo para continuar, ou deixe o nome em branco (\"pular conflitos\") para pular:", "fp_emore": "ainda há algumas colisões de nome de arquivo para consertar", "fp_ok": "movimento OK", "fcp_ok": "cópia OK", @@ -447,8 +447,8 @@ Ls.por = { "fcp_both_b": 'CopiarEnviar', "mk_noname": "digite um nome no campo de texto à esquerda antes de fazer isso :p", - "nmd_i1": "também pode adicionar a extensão desejada, por exemplo .md", //m - "nmd_i2": "só pode criar ficheiros .md porque não tem permissão para apagar", //m + "nmd_i1": "também adicione a extensão desejada, por exemplo .md", + "nmd_i2": "só pode criar arquivos .md porque não tem permissão para apagar", "tv_load": "Carregando documento de texto:\n\n{0}\n\n{1}% ({2} de {3} MiB carregados)", "tv_xe1": "não foi possível carregar o arquivo de texto:\n\nerro ", @@ -459,7 +459,7 @@ Ls.por = { "tvt_prev": "mostrar documento anterior$NHotkey: i\">⬆ anterior", "tvt_next": "mostrar próximo documento$NHotkey: K\">⬇ próximo", "tvt_sel": "selecionar arquivo   ( para recortar / copiar / excluir / ... )$NHotkey: S\">sel", - "tvt_j": "embelezar json$NHotkey: shift-J\">j", //m + "tvt_j": "embelezar json$NHotkey: shift-J\">j", "tvt_edit": "abrir arquivo no editor de texto$NHotkey: E\">✏️ editar", "tvt_tail": "monitorar arquivo para alterações; mostrar novas linhas em tempo real\">📡 seguir", "tvt_wrap": "quebra de linha\">↵", @@ -472,7 +472,7 @@ Ls.por = { "m3u_clip": "playlist m3u agora copiada para a área de transferência\n\nvocê deve criar um novo arquivo de texto chamado something.m3u e colar a playlist nesse documento; isso a tornará reproduzível", "gt_vau": "não mostrar vídeos, apenas tocar o áudio\">🎧", - "gt_msel": "ativar seleção de arquivo; ctrl-clique em um arquivo para substituir$N$n<em>quando ativo: clique duas vezes em um arquivo / pasta para abri-lo&>t;/em>$N$nHotkey: S\">multisseleção", + "gt_msel": "ativar seleção de arquivo; ctrl-clique em um arquivo para substituir$N$N<em>quando ativo: clique duas vezes em um arquivo / pasta para abri-lo&>t;/em>$N$NHotkey: S\">multisseleção", "gt_crop": "cortar miniaturas ao centro\">cortar", "gt_3x": "miniaturas de alta resolução\">3x", "gt_zoom": "zoom", @@ -530,11 +530,11 @@ Ls.por = { "fz_tar": "arquivo gnu-tar não comprimido (linux / mac)", "fz_pax": "tar de formato pax não comprimido (mais lento)", - "fz_targz": "gnu-tar com compressão gzip nível 3$N$nisto é geralmente muito lento, então$nuse tar não comprimido em vez disso", - "fz_tarxz": "gnu-tar com compressão xz nível 1$N$nisto é geralmente muito lento, então$nuse tar não comprimido em vez disso", + "fz_targz": "gnu-tar com compressão gzip nível 3$N$Nisto é geralmente muito lento, então$Nuse tar não comprimido em vez disso", + "fz_tarxz": "gnu-tar com compressão xz nível 1$N$Nisto é geralmente muito lento, então$Nuse tar não comprimido em vez disso", "fz_zip8": "zip com nomes de arquivos utf8 (pode ser instável no windows 7 e mais antigos)", "fz_zipd": "zip com nomes de arquivos cp437 tradicionais, para software realmente antigo", - "fz_zipc": "cp437 com crc32 calculado antecipadamente,$npara MS-DOS PKZIP v2.04g (outubro de 1993)$n(leva mais tempo para processar antes que o download possa começar)", + "fz_zipc": "cp437 com crc32 calculado antecipadamente,$Npara MS-DOS PKZIP v2.04g (outubro de 1993)$N(leva mais tempo para processar antes que o download possa começar)", "un_m1": "você pode excluir seus uploads recentes (ou abortar os que não foram concluídos) abaixo", "un_upd": "atualizar", @@ -644,25 +644,25 @@ Ls.por = { "ur_um": "Concluído;\n{0} uploads OK,\n{1} uploads falharam, desculpe", "ur_sm": "Concluído;\n{0} arquivos encontrados no servidor,\n{1} arquivos NÃO encontrados no servidor", - "rc_opn": "abrir", //m - "rc_ply": "reproduzir", //m - "rc_pla": "reproduzir como áudio", //m - "rc_txt": "abrir no visualizador de arquivos", //m - "rc_md": "abrir no editor de texto", //m - "rc_dl": "baixar", //m - "rc_zip": "baixar como arquivo", //m - "rc_cpl": "copiar link", //m - "rc_del": "excluir", //m - "rc_cut": "recortar", //m - "rc_cpy": "copiar", //m - "rc_pst": "colar", //m - "rc_rnm": "renomear", //m - "rc_nfo": "nova pasta", //m - "rc_nfi": "novo arquivo", //m - "rc_sal": "selecionar tudo", //m - "rc_sin": "inverter seleção", //m - "rc_shf": "compartilhar esta pasta", //m - "rc_shs": "compartilhar seleção", //m + "rc_opn": "abrir", + "rc_ply": "reproduzir", + "rc_pla": "reproduzir como áudio", + "rc_txt": "abrir no leitor de texto", + "rc_md": "abrir no leitor markdown", + "rc_dl": "baixar", + "rc_zip": "baixar compactado", + "rc_cpl": "copiar link", + "rc_del": "excluir", + "rc_cut": "recortar", + "rc_cpy": "copiar", + "rc_pst": "colar", + "rc_rnm": "renomear", + "rc_nfo": "nova pasta", + "rc_nfi": "novo arquivo", + "rc_sal": "selecionar tudo", + "rc_sin": "inverter seleção", + "rc_shf": "compartilhar esta pasta", + "rc_shs": "compartilhar seleção", "lang_set": "atualizar para a mudança ter efeito?", @@ -705,8 +705,8 @@ Ls.por = { "ta1": "primeiro digite sua nova senha", "ta2": "repita para confirmar a nova senha:", "ta3": "há um erro; por favor, tente novamente", - "nop": "ERRO: A senha não pode estar em branco", //m - "nou": "ERRO: O nome de usuário e/ou a senha não podem estar em branco", //m + "nop": "ERRO: A senha não pode estar em branco", + "nou": "ERRO: O nome de usuário e/ou a senha não podem estar em branco", "aa1": "arquivos de entrada:", "ab1": "desativar no304", "ac1": "ativar no304", From b20d32593e95aeddde21adcf5cebdd6a3bd03085 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 25 Jan 2026 16:26:23 +0000 Subject: [PATCH 50/54] prism: change language subset; * add nasm (+0.3 K) * rm autohotkey (-3.8 K) * rm cmake (-4.0 K) --- copyparty/web/browser.js | 7 ++++--- scripts/deps-docker/genprism.sh | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 5cb8a943..a616694d 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -4858,7 +4858,7 @@ var showfile = (function () { 'nrend': 0, }; r.map = { - '.ahk': 'autohotkey', + '.asm': 'nasm', '.bas': 'basic', '.bat': 'batch', '.cxx': 'cpp', @@ -4881,6 +4881,8 @@ var showfile = (function () { '.rs': 'rust', '.sh': 'bash', '.service': 'systemd', + '.socket': 'systemd', + '.timer': 'systemd', '.txt': 'ans', '.vb': 'vbnet', '.v': 'verilog', @@ -4889,10 +4891,9 @@ var showfile = (function () { '.yml': 'yaml' }; r.nmap = { - 'cmakelists.txt': 'cmake', 'dockerfile': 'docker' }; - var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nim py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig'; + var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nasm nim nix py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig'; x = x.split(/ +/g); for (var a = 0; a < x.length; a++) if (!r.map["." + x[a]]) diff --git a/scripts/deps-docker/genprism.sh b/scripts/deps-docker/genprism.sh index a0fca276..95aab73b 100755 --- a/scripts/deps-docker/genprism.sh +++ b/scripts/deps-docker/genprism.sh @@ -32,7 +32,7 @@ langs=( # aspnet # asm6502 # asmatmel - autohotkey + # autohotkey # autoit # avisynth # avro-idl @@ -60,7 +60,7 @@ langs=( # cilkc # cilkcpp # clojure - cmake + # cmake # cobol # coffeescript # concurnas @@ -183,7 +183,7 @@ langs=( # n4js # nand2tetris-hdl # naniscript - # nasm + nasm # neon # nevod # nginx From 296362fc846376c09872b5cb927164d6b468bbcc Mon Sep 17 00:00:00 2001 From: Josh Willox Date: Mon, 26 Jan 2026 03:31:45 +1100 Subject: [PATCH 51/54] webdav: x-oc-mtime as float (#1240); closes #1239 --- copyparty/bos/bos.py | 6 +++--- copyparty/httpcli.py | 2 +- tests/test_webdav.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/copyparty/bos/bos.py b/copyparty/bos/bos.py index 9013a066..7aadb2cf 100644 --- a/copyparty/bos/bos.py +++ b/copyparty/bos/bos.py @@ -106,14 +106,14 @@ def utime( def utime_c( log: Union["NamedLogger", Any], p: str, - ts: int, + ts: float, follow_symlinks: bool = True, throw: bool = False, -) -> Optional[int]: +) -> Optional[float]: clamp = 0 ov = ts bp = fsenc(p) - now = int(time.time()) + now = time.time() while True: try: if SYMTIME: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8e26701c..a5de9e4d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -2618,7 +2618,7 @@ class HttpCli(object): at = mt = time.time() - lifetime cli_mt = self.headers.get("x-oc-mtime") if cli_mt: - bos.utime_c(self.log, path, int(cli_mt), False) + bos.utime_c(self.log, path, float(cli_mt), False) if nameless and "magic" in vfs.flags: try: diff --git a/tests/test_webdav.py b/tests/test_webdav.py index d7618a03..7714b61b 100644 --- a/tests/test_webdav.py +++ b/tests/test_webdav.py @@ -63,6 +63,20 @@ Accept-Encoding: gzip fgsfds""" +RCLONE_PUT_FLOAT = """PUT /%s HTTP/1.1 +Host: 127.0.0.1:3923 +User-Agent: rclone/v1.67.0 +Content-Length: 6 +Authorization: Basic azp1 +Content-Type: application/octet-stream +Oc-Checksum: SHA1:f5e3dc3fb27af53cd0005a1184e2df06481199e8 +Referer: http://127.0.0.1:3923/ +X-Oc-Mtime: 1689453578.123 +Accept-Encoding: gzip + +fgsfds""" + + # tcpdump of `rclone delete dav:/a/d1/` (it does propfind recursively and then this on each file) # (note: `rclone rmdirs dav:/a/d1/` does the same thing but just each folder after asserting they're empty) RCLONE_DELETE = """DELETE /%s HTTP/1.1 @@ -201,6 +215,11 @@ class TestHttpCli(TC): h, b = self.req(RCLONE_PUT % ("a/fa",)) self.assertStart("HTTP/1.1 201 Created\r", h) + # float x-oc-mtime should be accepted + h, b = self.req(RCLONE_PUT_FLOAT % ("a/fb",)) + self.assertStart("HTTP/1.1 201 Created\r", h) + self.assertAlmostEqual(os.path.getmtime("a/fb"), 1689453578.123, places=3) + # then it does a propfind to confirm h, b = self.req(RCLONE_PROPFIND % ("a/fa",)) fns = pfind2ls(b) From 8e046fb6a8118162feb4a365196fff2fba1c00d1 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 25 Jan 2026 16:35:15 +0000 Subject: [PATCH 52/54] webdav: fix macos-finder connect delay (closes #1242); if both `quota-available-bytes` and `quotaused` are ignored (not even returned as 404), then macos Finder is able to connect instantly, avoiding this longstanding bug in macos the presence of `quotaused` is the trigger for this logic, which is a property apple invented and only apple uses, meaning we can safely break the webdav spec as required in this case thx @freddyheppell for the observation --- copyparty/httpcli.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index a5de9e4d..3ab43d37 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1833,7 +1833,9 @@ class HttpCli(object): zi = ( vn.flags["du_iwho"] - if vn.realpath and "quota-available-bytes" in props + if vn.realpath + and "quota-available-bytes" in props + and "quotaused" not in props # macos finder; ingnore it else 0 ) if zi and ( @@ -1864,10 +1866,6 @@ class HttpCli(object): "quota-available-bytes": str(bfree), "quota-used-bytes": str(btot - bfree), } - if "quotaused" in props: # macos finder crazytalk - df["quotaused"] = df["quota-used-bytes"] - if "quota" in props: - df["quota"] = df["quota-available-bytes"] # idk, makes it happy else: df = {} else: From 24141b494b9b22d0ec50a59f19d66d6112f1a0d8 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 25 Jan 2026 21:38:18 +0000 Subject: [PATCH 53/54] another request-smuggling failsafe; could concievably help when behind a buggy reverseproxy --- copyparty/httpcli.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 3ab43d37..f0ffb1a1 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -342,11 +342,6 @@ class HttpCli(object): if not headerlines: return False - if not headerlines[0]: - # seen after login with IE6.0.2900.5512.xpsp.080413-2111 (xp-sp3) - self.log("BUG: trailing newline from previous request", c="1;31") - headerlines.pop(0) - try: self.mode, self.req, self.http_ver = headerlines[0].split(" ") @@ -355,6 +350,8 @@ class HttpCli(object): for header_line in headerlines[1:]: k, zs = header_line.split(":", 1) self.headers[k.lower()] = zs.strip() + if zs.endswith(" HTTP/1.1"): + raise Exception() except: headerlines = [repr(x) for x in headerlines] msg = "#[ " + " ]\n#[ ".join(headerlines) + " ]" From b6bf6d5f7a5c10a00d161daff9b82fcfdf3d508e Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 25 Jan 2026 21:40:16 +0000 Subject: [PATCH 54/54] shares: fix lifetime-extend; closes #1248 --- copyparty/web/shares.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/web/shares.js b/copyparty/web/shares.js index 783b4561..b0669318 100644 --- a/copyparty/web/shares.js +++ b/copyparty/web/shares.js @@ -16,7 +16,7 @@ function rm() { } function bump() { - var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'), + var k = this.closest('tr').querySelector('a[k]').getAttribute('k'), u = SR + '/?skey=' + uricom_enc(k) + '&eshare=' + this.value, xhr = new XHR();