From 3931bc27791658449f6dc3fc24e3a53d43d62a49 Mon Sep 17 00:00:00 2001 From: Kent Daleng Date: Fri, 8 Aug 2025 22:54:24 +0200 Subject: [PATCH 001/154] =?UTF-8?q?legg=20=C3=A5t=20nynorskoversetjing=20(?= =?UTF-8?q?#537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * legg åt nynorskoversetjing, og fikser et par typos på bokmål også :) * add nno to splash, fix a few more stray typos * more til -> åt --------- Signed-off-by: Kent D --- copyparty/web/browser.js | 640 ++++++++++++++++++++++++++++++++++++++- copyparty/web/splash.js | 42 +++ 2 files changed, 677 insertions(+), 5 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index c9f9e70b..70ee0acf 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -853,7 +853,7 @@ var Ls = { "cl_hcancel": "kolonne-skjuling avbrutt", "ct_grid": '田 ikoner', - "ct_ttips": 'hvis hjelpetekst ved å holde musen over ting">ℹ️ tips', + "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_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯', @@ -878,8 +878,8 @@ var Ls = { "cut_az": "last opp filer i alfabetisk rekkefølge, istedenfor minste-fil-først$N$Nalfabetisk kan gjøre det lettere å anslå om alt gikk bra, men er bittelitt tregere på fiber / LAN", - "cut_nag": "meldingsvarsel når opplastning er ferdig$N(kun on nettleserfanen ikke er synlig)", - "cut_sfx": "lydvarsel når opplastning er ferdig$N(kun on nettleserfanen ikke er synlig)", + "cut_nag": "meldingsvarsel når opplastning er ferdig$N(kun om nettleserfanen ikke er synlig)", + "cut_sfx": "lydvarsel når opplastning er ferdig$N(kun om nettleserfanen ikke er synlig)", "cut_mt": "raskere befaring ved å bruke hele CPU'en$N$Ndenne funksjonen anvender web-workers$Nog krever mer RAM (opptil 512 MiB ekstra)$N$Ngjør https 30% raskere, http 4.5x raskere\">mt", @@ -913,7 +913,7 @@ var Ls = { "mt_loop": "spill den samme sangen om og om igjen\">🔁", "mt_one": "spill kun én sang\">1️⃣", "mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefølge\">🔀", - "mt_aplay": "forsøk å starte avspilling hvis linken du klikket på for å åpne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres så vil heller ikke nettside-URLen bli oppdatert med sang-ID'er når musikk spilles, i tilfelle innstillingene skulle gå tapt og nettsiden lastes på ny\">a▶", + "mt_aplay": "forsøk å starte avspilling hvis linken du klikket på for å åpne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres så vil heller ikke nettside-URL'en bli oppdatert med sang-ID'er når musikk spilles, i tilfelle innstillingene skulle gå tapt og nettsiden lastes på ny\">a▶", "mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles", "mt_prescan": "ved behov, bla til neste mappe$Nslik at nettleseren lar oss$Nfortsette å spille musikk\">bla", "mt_fullpre": "hent ned hele neste sang, ikke bare litt:$N✅ skru på hvis nettet ditt er ustabilt,$N❌ skru av hvis nettet ditt er tregt\">full", @@ -5672,6 +5672,636 @@ var Ls = { "lang_set": "Vernieuw de pagina om de wijziging door te voeren?", }, + "nno": { + "tt": "Nynorsk", + + "cols": { + "c": "handlingsknappar", + "dur": "varigheit", + "q": "kvalitet / bitrate", + "Ac": "lydformat", + "Vc": "videoformat", + "Fmt": "format / innpakning", + "Ahash": "lydkontrollsum", + "Vhash": "videokontrollsum", + "Res": "oppløysing", + "T": "filtype", + "aq": "lydkvalitet / bitrate", + "vq": "videokvalitet / bitrate", + "pixfmt": "fargekoding / detaljnivå", + "resw": "horisontal oppløysing", + "resh": "vertikal oppløysing", + "chs": "lydkanaler", + "hz": "lydoppløsing" + }, + + "hks": [ + [ + "ymse", + ["ESC", "lukk saker og ting"], + + "filbehandlar", + ["G", "listevisning eller ikon"], + ["T", "miniatyrbilder på/av"], + ["⇧ A/D", "ikonstorleik"], + ["ctrl-K", "slett valde"], + ["ctrl-X", "klipp ut valde"], + ["ctrl-C", "kopiér åt utklippstavle"], + ["ctrl-V", "lim inn (flytt/kopiér)"], + ["Y", "last ned valde"], + ["F2", "endre namn på valde"], + + "filmarkering", + ["space", "markér fil"], + ["↑/↓", "flytt markør"], + ["ctrl ↑/↓", "flytt markør og scroll"], + ["⇧ ↑/↓", "velg forr./neste fil"], + ["ctrl-A", "velg alle filer / mapper"], + ], [ + "navigering", + ["B", "mappehierarki eller filsti"], + ["I/K", "forr./neste mappe"], + ["M", "eitt nivå opp (eller lukk)"], + ["V", "vis mapper eller tekstfiler"], + ["A/D", "panelstorleik"], + ], [ + "musikkspelar", + ["J/L", "forr./neste song"], + ["U/O", "hopp 10sek bak/fram"], + ["0..9", "hopp åt 0%..90%"], + ["P", "pause, eller start / fortsett"], + ["S", "marker spelande song"], + ["Y", "last ned song"], + ], [ + "bildevisar", + ["J/L, ←/→", "forr./neste bilde"], + ["Home/End", "første/siste bilde"], + ["F", "fullskjermvisning"], + ["R", "rotér åt høyre"], + ["⇧ R", "rotér åt venstre"], + ["S", "markér bilde"], + ["Y", "last ned bilde"], + ], [ + "videospelar", + ["U/O", "hopp 10sek bak/fram"], + ["P/K/Space", "pause / fortsett"], + ["C", "fortsett åt neste fil"], + ["V", "gjenta avspeling"], + ["M", "lyd av/på"], + ["[ og ]", "gjentaksintervall"], + ], [ + "dokumentvisar", + ["I/K", "forr./neste fil"], + ["M", "lukk tekstdokument"], + ["E", "redigér tekstdokument"], + ["S", "markér fil (for F2/ctrl-x/...)"], + ["Y", "last ned tekstfil"], + ] + ], + + "m_ok": "OK", + "m_ng": "Avbryt", + + "enable": "Aktiv", + "danger": "VARSKU", + "clipped": "kopiert åt utklippstavla", + + "ht_s1": "sekund", + "ht_s2": "sekund", + "ht_m1": "minutt", + "ht_m2": "minutt", + "ht_h1": "time", + "ht_h2": "timar", + "ht_d1": "dag", + "ht_d2": "dagar", + "ht_and": " og ", + + "goh": "kontrollpanel", + "gop": 'navigér åt mappa før den her">forr.', + "gou": 'navigér eitt nivå opp">opp', + "gon": 'navigér åt mappa etter den her">neste', + "logout": "Logg ut ", + "access": " åtgang", + "ot_close": "lukk reiskap", + "ot_search": "søk etter filer ved å angje filnamn, mappenamn, tid, storleik, eller metadata som songtittel / artist / osv.$N$N<code>foo bar</code> = inneheld båe «foo» og «bar»,$N<code>foo -bar</code> = innehold «foo» men ikkje «bar»,$N<code>^yana .opus$</code> = startar med «yana», filtype «opus»$N<code>"try unite"</code> = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", + "ot_unpost": "unpost: slett filer som du nyleg har lastet opp; «angre-knappen»", + "ot_bup": "bup: tradisjonell / primitiv filopplasting,$N$Nfungerar i om lag samtlege nettlesarar", + "ot_mkdir": "mkdir: lag ei ny mappe", + "ot_md": "new-md: lag eit nytt markdown-dokument", + "ot_msg": "msg: send ein beskjed åt serverloggen", + "ot_mp": "musikkspelarinstillinger", + "ot_cfg": "andre innstillinger", + "ot_u2i": 'up2k: last opp filer (viss du har skriveåtgang) eller bytt åt søkemodus for å sjekke om filene finnast ein eller annan plass på serveren$N$Nopplastinger kan startast opp att etter avbrot, skjer stykkevis for potensielt høgare ytelse, og ivaretek datostempling -- men bruker litt meir prosessorkraft enn [🎈]  (den primitive opplastaren "bup")

mens opplastinger føregår så visast framdrifta her oppe!', + "ot_u2w": 'up2k: filopplasting med støtte for å starte opp att avbrotne opplastinger -- steng ned nettlesaren og drage dei same filene inn i nettlesaren igjen for å plukke opp att der du slapp$N$Nopplastinger skjer stykkevis for potensielt høgare ytelse, og ivaretek datostempling -- men bruker litt meir prosessorkraft enn [🎈]  (den primitive opplastaren "bup")

mens opplastinger føregår så visast framdrifta her oppe!', + "ot_noie": 'Fungerer mye betre i Chrome / Firefox / Edge', + + "ab_mkdir": "lag mappe", + "ab_mkdoc": "nytt dokument", + "ab_msg": "send melding", + + "ay_path": "gå videre åt mapper", + "ay_files": "gå videre åt filer", + + "wt_ren": "gje nye namn åt dei valde filene$NSnarvei: F2", + "wt_del": "slett dei valde filene$NSnarvei: ctrl-K", + "wt_cut": "klipp ut dei valde filene <small>(for å lime inn ein annan plass)</small>$NSnarvei: ctrl-X", + "wt_cpy": "kopiér dei valde filene åt utklippstavla$N(for å lime inn ein annan plass)$NSnarvei: ctrl-C", + "wt_pst": "lim inn filer (som tidligare blei klipt ut / kopiert ein annan plass)$NSnarvei: ctrl-V", + "wt_selall": "velg alle filer$NSnarvei: ctrl-A (mens fokus er på ei fil)", + "wt_selinv": "invertér utval", + "wt_zip1": "last ned denne mappa som eit arkiv", + "wt_selzip": "last ned dei valde filene som eit arkiv", + "wt_seldl": "last ned dei valde filene$NSnarvei: Y", + "wt_npirc": "kopiér songinfo (irc-formatert)", + "wt_nptxt": "kopiér songinfo", + "wt_m3ua": "legg song åt i m3u-speleliste$N(husk å klikk på 📻copy senere)", + "wt_m3uc": "kopiér m3u-spelelista åt utklippstavla", + "wt_grid": "bytt mellom ikon og listevising$NSnarvei: G", + "wt_prev": "førre song$NSnarvei: J", + "wt_play": "play / pause$NSnarvei: P", + "wt_next": "neste song$NSnarvei: L", + + "ul_par": "samtidige handl.:", + "ut_rand": "finn opp nye tilfeldige filnamn", + "ut_u2ts": "gje fila på serveren same$Ntidsstempel som lokalt hos deg\">📅", + "ut_ow": "overskrive eksisterande filer på serveren?$N🛡️: aldri (finn på eit nytt filnamn i staden for)$N🕒: overskriv viss fila åt serveren er eldre$N♻️: alltid, gitt at innholdet er annleis", + "ut_mt": "fortsett å synfare køa mens opplasting føregår$N$Nskru denne av dersom du har ein$Ntreig prosessor eller harddisk", + "ut_ask": 'bekreft filutvalg før opplasting startar">💭', + "ut_pot": "forbetre ytinga på treige einheiter ved å$Nforenkle brukergrensesnittet", + "ut_srch": "gjer eit søk i staden for å laste opp --$Nleitar gjennom alle mappane du har lov åt å sjå", + "ut_par": "sett åt 0 for å midlertidig stoppe opplasting$N$Nhøge verdier (4 eller 8) kan gje betre yting,$Nspesielt på treige internettlinjer$N$Nbør ikkje vere høgare enn 1 på LAN$Neller viss serveren sin harddisk er treig", + "ul_btn": "slepp filer / mapper
her (eller klikk meg)", + "ul_btnu": "L A S T   O P P", + "ul_btns": "F I L S Ø K", + + "ul_hash": "synfar", + "ul_send": " send", + "ul_done": "total", + "ul_idle1": "ingen handlinger i køen", + "ut_etah": "snitthastigheit for <em>synfaring</em> samt gjenståande tid", + "ut_etau": "snitthastigheit for <em>opplasting</em> samt gjenståande tid", + "ut_etat": "<em>total</em> snitthastigheit og gjenståande tid", + + "uct_ok": "fullført uten problem", + "uct_ng": "fullført under tvil (duplikat, ikkje funne, ...)", + "uct_done": "fullført (enten <em>ok</em> eller <em>ng</em>)", + "uct_bz": "aktive handlinger (synfaring / opplasting)", + "uct_q": "køa", + + "utl_name": "filnamn", + "utl_ulist": "vis", + "utl_ucopy": "kopiér", + "utl_links": "lenker", + "utl_stat": "status", + "utl_prog": "fremdrift", + + // må vere korte: + "utl_404": "404", + "utl_err": "FEIL!", + "utl_oserr": "OS-feil", + "utl_found": "funnet", + "utl_defer": "seinare", + "utl_yolo": "YOLO", + "utl_done": "ferdig", + + "ul_flagblk": "filene har blitt lagd i køa
men det er ein anna nettlesarfane som held på med synfaring eller opplasting akkurat no,
så venter åt den er ferdig først", + "ul_btnlk": "brytaren har blitt låst åt denne tilstanden i serverens konfigurasjon", + + "udt_up": "Last opp", + "udt_srch": "Søk", + "udt_drop": "Slepp filene her", + + "u_nav_m": '
kva har du?
Enter = Filer (éin eller fleire)\nESC = Éi mappe (inkludert undermapper)', + "u_nav_b": 'FilerÉi mappe', + + "cl_opts": "brytarar", + "cl_themes": "utsjånad", + "cl_langs": "språk", + "cl_ziptype": "nedlasting av mapper", + "cl_uopts": "up2k-brytarar", + "cl_favico": "favicon", + "cl_bigdir": "store mapper", + "cl_hsort": "#sort", + "cl_keytype": "notasjon for musikalsk dur", + "cl_hiddenc": "skjulte kolonner", + "cl_hidec": "skjul", + "cl_reset": "nullstill", + "cl_hpick": "klikk på overskrifta åt kolonnene du ønskjer å skjule i tabellen nedanfor", + "cl_hcancel": "kolonne-skjuling avbrote", + + "ct_grid": '田 ikon', + "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_ihop": 'bla ned åt sist viste bilde når bildevisaren lukkast">g⮯', + "ct_dots": 'vis skjulte filer (gitt at serveren tillèt det)">.synlig', + "ct_qdel": 'sletteknappen spør berre éin gong om stadfesting">hurtig🗑️', + "ct_dir1st": 'sortér slik at mapper kjem framanfor filer">📁 først', + "ct_nsort": 'naturlig sortering (skjønar tal i filnamn)">nsort', + "ct_utc": 'bruk UTC for alle klokkeslett">UTC', + "ct_readme": 'vis README.md nedanfor filene">📜 readme', + "ct_idxh": 'vis index.html i staden for filliste">htm', + "ct_sbars": 'vis rullgardiner / skrollefelt">⟊', + + "cut_umod": 'i tilfelle ei fil du lastar opp alt finnast på serveren, så skal tidsstempelet åt serveren oppdaterast slik at det stemmer overeins med din lokale fil (krev rettigheitene write+delete)">re📅', + + "cut_turbo": "forenkla synfaring ved opplasting; bør etter alt å døme ikkje skruast på:$N$Nnyttig dersom du var midt i ei svær opplasting som måtte startast på nytt av ein eller annan grunn, og du vil komme i gang igjen så raskt som i det heile mulig.$N$Nnår denne er skrudd på så forenklast synfaringa kraftig; i staden for å utføre ein trygg sjekk på om filene finnast på serveren i god stand, så sjekkast det kun om filstorleiken stemmer. Så dersom ein korrupt fil vere på serveren allerede, på same plass, med same storleik og namn, så blir det ikkje oppdaga.$N$Ndet anbefalast å kun benytte denne funksjonen for å komme seg raskt gjennom sjølve opplastinga, for så å skru den av, og åt slutt "laste opp" dei same filene éin gong åt -- slik at integriteten kan verifiserast\">turbo", + + "cut_datechk": "har ingen effekt dersom turbo er skrudd av$N$Ngjer turbo bittelitt tryggare ved å sjekke datostemplinga på filene (i tillegg åt filstorleik)$N$Nburde oppdage og gjenoppta dei fleste ufullstendige opplastinger, men er ikkje ein fullverdig erstatning for å deaktivere turbo og gjere ein skikkeleg sjekk\">date-chk", + + "cut_u2sz": "storleik i megabyte for kvart bruddstykke for opplasting. Store verdiar flyg betre over atlanteren. Små verdiar kan vere betre på flettande ustabile samband", + + "cut_flag": "samkøyrer nettlesarfaner slik at berre éin $N kan holde på med synfaring / opplasting $N -- andre faner må óg ha denne skrudd på $N -- fungerar kun innanom same domene", + + "cut_az": "last opp filer i alfabetisk rekkefølge, i staden for minste-fil-først$N$Nalfabetisk kan gjere det lettare å anslå om alt gjekk bra, men er bittelitt treigare på fiber / LAN", + + "cut_nag": "meldingsvarsel når opplasting er ferdig$N(kun om nettlesarfana ikkje er synlig)", + "cut_sfx": "lydvarsel når opplasting er ferdig$N(kun om nettlesarfanen ikkje er synlig)", + + "cut_mt": "raskere synfaring ved å bruke heile CPU'en$N$Ndenne funksjonen nytter web-workers$Nog krev meir RAM (opptil 512 MiB ekstra)$N$Ngjer https 30% raskare, http 4.5x raskare\">mt", + + "cut_wasm": "bruk wasm i staden for nettlesaren sin sha512-funksjon; gjev betre yting på chrome-baserte nettlesarar, men brukar meir CPU, og eldre versjoner av chrome toler det ikkje (et opp all RAM og kræsjer)\">wasm", + + "cft_text": "ikontekst (blank ut og last siden på nytt for å deaktivere)", + "cft_fg": "farge", + "cft_bg": "bakgrunnsfarge", + + "cdt_lim": "maks mengd filer å vise per mappe", + "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", + + "tt_entree": "bytt åt mappehierarki$NSnarvei: B", + "tt_detree": "bytt åt tradisjonell stivising$NSnarvei: B", + "tt_visdir": "bla ned åt den åpne mappa", + "tt_ftree": "bytt mellom filstruktur og tekstfiler$NSnarvei: V", + "tt_pdock": "vis dei overordna mappane i eit panel", + "tt_dynt": "øk bredda på panelet ettersom treet utvider seg", + "tt_wrap": "linjebryting", + "tt_hover": "vis heile mappenamnet når musepeikaren treff mappa$N( gjer diverre at scrollhjulet fusker dersom musepeikaren ikkje finn seg i grøfta )", + + "ml_pmode": "ved enden av mappa", + "ml_btns": "knapper", + "ml_tcode": "konvertering", + "ml_tcode2": "konvertér til", + "ml_tint": "tint", + "ml_eq": "audio equalizer (tonejustering)", + "ml_drc": "compressor (volumutjevning)", + + "mt_loop": "spel den same songen om og om igjen\">🔁", + "mt_one": "spel kun éin song\">1️⃣", + "mt_shuf": "songane i kvar mappe$Nspelast i tilfeldig rekkefølge\">🔀", + "mt_aplay": "prøv å starte avspeling viss linken du trykte på for å åpne nettsida inneheld ein song-ID$N$Nviss denne deaktiverast så vil heller ikkje nettside-URL'en bli oppdatert med song-ID'er når musikk spelast, i tilfelle innstillingane skulle gå tapt og nettsida lastast på ny\">a▶", + "mt_preload": "hent ned litt av neste song i forkant,$Nslik at pausa i overgangen blir mindre\">forsyn", + "mt_prescan": "ved behov, bla åt neste mappe$Nslik at nettlesaren lar oss$Nfortsetja å spele musikk\">bla", + "mt_fullpre": "hent ned heile neste song, ikkje berre litt:$N✅ skru på viss nettet ditt er ustabilt,$N❌ skru av viss nettet ditt er treigt\">full", + "mt_fau": "for telefoner: forhindre at avspeling stoppar viss nettet er for treigt åt å laste neste song i tide. Viss påskrudd kan det forårsake at songinfo ikkje visast korrekt i OS'et\">☕️", + "mt_waves": "waveform seekbar:$Nvis volumkurve i avspelingsfeltet\">~s", + "mt_npclip": "vis knappar for å kopiere info om songen du høyrer på\">/np", + "mt_m3u_c": "vis knapper for å kopiere dei valde$Nsongene som innslag i ei m3u8-speleliste\">📻", + "mt_octl": "integrering med operativsystemet (fjernkontroll, infoskjerm)\">os-ctl", + "mt_oseek": "gje løyve åt spoling med fjernkontroll$N$Nmerk: på nokon eininger (iPhones) så vil$Ndette erstatte knappen for neste song\">spoling", + "mt_oscv": "vis albumcover på infoskjermen\">bilde", + "mt_follow": "bla slik at songen som spelast alltid er synleg\">🎯", + "mt_compact": "tettpakka spelarpanel\">⟎", + "mt_uncache": "prøv denne viss ein song ikkje spelar riktig\">oppfrisk", + "mt_mloop": "repetér heile mappa\">🔁 gjenta", + "mt_mnext": "hopp åt neste mappe og fortsett\">📂 neste", + "mt_mstop": "stopp avspeling\">⏸ stopp", + "mt_cflac": "konvertér flac / wav-filer åt opus\">flac", + "mt_caac": "konvertér aac / m4a-filer åt to opus\">aac", + "mt_coth": "konvertér alt anna (men ikkje mp3) åt opus\">andre", + "mt_c2opus": "det beste valget for alle PCar og Android\">opus", + "mt_c2owa": "opus-weba, for iOS 17.5 og nyare\">owa", + "mt_c2caf": "opus-caf, for iOS 11 åt og med 17\">caf", + "mt_c2mp3": "bra valg for steinalder-utstyr (slår aldri feil)\">mp3", + "mt_c2flac": "gir best lydkvalitet, men et nettet ditt\">flac", + "mt_c2wav": "heilt rå lydstrøm (bruker enda meir data enn flac)\">wav", + "mt_c2ok": "bra valg!", + "mt_c2nd": "ikkje det føretrekte valget for din einheit, men funker sikkert greit", + "mt_c2ng": "ser verkelig ikkje ut som enheiten din taklar dette formatet... men ok, vi prøver", + "mt_xowa": "iOS har fortsatt problem med avspeling av owa-musikk i bakgrunnen. Bruk caf eller mp3 i staden for", + "mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjer oppdateringer mindre distraherande", + "mt_eq": "aktivér tonekontroll og forsterker;$N$Nboost <code>0</code> = normal volumskala$N$Nwidth <code>1  </code> = normal stereo$Nwidth <code>0.5</code> = 50% blanding venstre-høgre$Nwidth <code>0  </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = instrumental :^)$N$Nreduserer óg daudtid mellom songfiler", + "mt_drc": "aktivér volum-utjevning (dynamic range compressor); vil óg aktivere tonejustering, så sett alle EQ-feltene bortsett frå 'width' åt 0 viss du ikkje vil ha nokon EQ$N$Nfilteret vil dempe volumet på alt som er høgare enn TRESH dB; for kvar RATIO dB over grensa er det 1dB som treff høgtalarane, så standardverdiane tresh -24 og ratio 12 skal bety at volumet ikkje gjeng høgare enn -22 dB, slik at ein trygt kan øke boost-verdien i equalizeren åt rundt 0.8, eller 1.8 kombinert med ATK 0 og RLS 90 (berre mulig i firefox; andre nettlesarar tek ikkje høgare RLS enn 1)$N$Nwikipedia forklarar dette mykje betre forresten", + + "mb_play": "lytt", + "mm_hashplay": "spel denne songen?", + "mm_m3u": "trykk Enter/OK for å spele\ntrykk ESC/Avbryt for å redigere", + "mp_breq": "krev firefox 82+, chrome 73+, eller iOS 15+", + "mm_bload": "lastar inn...", + "mm_bconv": "konverterer åt {0}, vent litt...", + "mm_opusen": "nettlesaren din skjønar ikkje aac / m4a;\nkonvertering åt opus er no aktivert", + "mm_playerr": "avspeling feilet: ", + "mm_eabrt": "Avspelingsforespørselen blei avbroten", + "mm_enet": "Nettet ditt er ustabilt", + "mm_edec": "Noko er galt med musikkfila", + "mm_esupp": "Nettleseren din skjønar ikkje filtypen", + "mm_eunk": "Ukjent feil", + "mm_e404": "Avspeling feilet: Fil ikkje funnet.", + "mm_e403": "Avspeling feilet: Høve nekta.\n\nKanskje du blei logget ut?\nPrøv å trykk F5 for å laste sida på nytt.", + "mm_e500": "Avspeling feilet: Rusk i maskineriet, sjekk serverloggen.", + "mm_e5xx": "Avspeling feilet: ", + "mm_nof": "finn ikkje flere songer i nærheita", + "mm_prescan": "Leitar etter neste song...", + "mm_scank": "Fann neste song:", + "mm_uncache": "alle songer vil lastast på nytt ved neste avspeling", + "mm_hnf": "songen finnast ikkje lenger", + + "im_hnf": "bildet finnast ikkje lenger", + + "f_empty": 'denne mappa er tom', + "f_chide": 'dette vil skjule kolonna «{0}»\n\nfana for "andre innstillinger" let deg vise kolonna igjen', + "f_bigtxt": "denne fila er heeile {0} MiB -- vis som tekst?", + "f_bigtxt2": "vil du sjå bunnen av filen i staden for? du vil da óg sjå nye linjer som blir lagd åt på slutten av filen i sanntid", + "fbd_more": '
visar {0} av {1} filer; vis {2} eller vis alle
', + "fbd_all": '
visar {0} av {1} filer; vis alle
', + "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_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.", + + "ft_paste": "Lim inn {0} filer$NSnarvei: ctrl-V", + "fr_eperm": 'kan ikkje endre namn:\ndu har ikkje høve åt “move” i denne mappa', + "fd_eperm": 'kan ikkje slette:\ndu har ikkje høve åt “delete” i denne mappa', + "fc_eperm": 'kan ikkje klippe ut:\ndu har ikkje høve åt “move” i denne mappa', + "fp_eperm": 'kan ikkje lime inn:\ndu har ikkje høve åt “write” i denne mappa', + "fr_emore": "vel minst éi fil som skal få nytt namn", + "fd_emore": "vel minst éi fil som skal slettast", + "fc_emore": "vel minst éi fil som skal klippast ut", + "fcp_emore": "vel minst éi fil som skal kopierast åt utklippstavla", + + "fs_sc": "del mappa du er i no", + "fs_ss": "del dei valde filene", + "fs_just1d": "du kan ikkje markere flere mapper samtidig,\neller kombinere mapper og filer", + "fs_abrt": "❌ avbryt", + "fs_rand": "🎲 tilfeldig namn", + "fs_go": "✅ opprett deling", + "fs_name": "namn", + "fs_src": "kjelde", + "fs_pwd": "passord", + "fs_exp": "varigheit", + "fs_tmin": "min", + "fs_thrs": "timar", + "fs_tdays": "dagar", + "fs_never": "for evig", + "fs_pname": "valfri namn (blir litt tilfeldig ellers)", + "fs_tsrc": "fil/mappe som skal delast", + "fs_ppwd": "valfri passord", + "fs_w8": "opprettar deling...", + "fs_ok": "trykk Enter/OK for å kopiere lenka (for CTRL-V)\ntrykk ESC/Avbryt for å kun bekrefta", + + "frt_dec": "kan korrigere visse ødelagte filnamn\">url-decode", + "frt_rst": "nullstillar endringar (tilbake åt dei originale filnamna)\">↺ reset", + "frt_abrt": "avbryt og lukk dette vindauget\">❌ avbryt", + "frb_apply": "IVERKSETT", + "fr_adv": "automasjon basert på metadata
og / eller mønster (regulære uttrykk)\">avansert", + "fr_case": "versalfølsomme uttrykk\">Aa", + "fr_win": "bytt ut bokstavane <>:"\\|?* med$Ntilsvarande som windows ikkje får panikk av\">win", + "fr_slash": "bytt ut bokstaven / slik at den ikkje forårsakar at nye mapper opprettes\">ikke /", + "fr_re": "regex-mønster som køyrast på kvart filnamn. Grupper kan leses ut i format-feltet nedanfor, f.eks. <code>(1)</code> og <code>(2)</code> osv.", + "fr_fmt": "inspirert av foobar2000:$N<code>(title)</code> byttast ut med songtittel,$N<code>[(artist) - ](title)</code> dropper [dette] viss artist er blank$N<code>$lpad((tn),2,0)</code> visar songnr. med 2 siffer", + "fr_pdel": "slett", + "fr_pnew": "lagre som", + "fr_pname": "gje innstillingane dine eit namn", + "fr_aborted": "avbrote", + "fr_lold": "gamalt namn", + "fr_lnew": "nytt namn", + "fr_tags": "metadata for dei valde filene (kun for referanse):", + "fr_busy": "endrar namn på {0} filer...\n\n{1}", + "fr_efail": "endring av namn feila:\n", + "fr_nchg": "{0} av namna blei justert pga. win og/eller ikkje /\n\nvil du fortsetja med dei nye namna som blei valde?", + + "fd_ok": "sletting OK", + "fd_err": "sletting feila:\n", + "fd_none": "ingenting blei sletta; kanskje avvist av serverkonfigurasjon (xbd)?", + "fd_busy": "slettar {0} filer...\n\n{1}", + "fd_warn1": "SLETT disse {0} filene?", + "fd_warn2": "Siste sjanse! Dette kan ikkje angrast. Slett?", + + "fc_ok": "klipte ut {0} filer", + "fc_warn": 'klipte ut {0} filer\n\nmen: kun denne nettlesarfana har muligheit åt å lime dei inn ein annan plass, siden antallet filer er helt hinsides', + + "fcc_ok": "kopierte {0} filer åt utklippstavla", + "fcc_warn": 'kopierte {0} filer åt utklippstavla\n\nmen: kun denne nettlesarfana har muligheit åt å lime dei inn ein annan plass, sidan antallet filer er heilt på hi sida', + + "fp_apply": "bekreft og lim inn no", + "fp_ecut": "du må klippe ut eller kopiere nokre filer / mapper først\n\nmerk: du kan gjerne jobbe på kryss av nettlesarfaner; klippe ut i éi fane, lime inn i ei anna", + "fp_ename": "{0} filer kan ikkje flyttast åt målmappa fordi det allereie finnast filer med same namn. Gi dei nye namn nedanfor, eller gje dei eit blankt namn for å hoppe over dei:", + "fcp_ename": "{0} filer kan ikkje kopierast åt målmappa fordi det allereie finnast filer med same namn. Gi dei nye namn nedanfor, eller gje dei eit blankt namn for å hoppe over dei:", + "fp_emore": "det er fortsatt fleire namn som må endrast", + "fp_ok": "flytting OK", + "fcp_ok": "kopiering OK", + "fp_busy": "flyttar {0} filer...\n\n{1}", + "fcp_busy": "kopierar {0} filer...\n\n{1}", + "fp_err": "flytting feila:\n", + "fcp_err": "kopiering feila:\n", + "fp_confirm": "flytt disse {0} filene hit?", + "fcp_confirm": "kopiér disse {0} filene hit?", + "fp_etab": 'kunne ikkje lese lista med filer frå den andre nettlesarfana', + "fp_name": "Lastar opp éi fil frå einheita di. Velg filnamn:", + "fp_both_m": '
kva skal limast inn her?
Enter = Flytt {0} filer frå «{1}»\nESC = Last opp {2} filer frå einheita din', + "fcp_both_m": '
kva skal limes inn her?
Enter = Kopiér {0} filer frå «{1}»\nESC = Last opp {2} filer frå einheita din', + "fp_both_b": 'FlyttLast opp', + "fcp_both_b": 'KopiérLast opp', + + "mk_noname": "skriv inn eit namn i tekstboksa åt venstre først :p", + + "tv_load": "Lastar inn tekstfil:\n\n{0}\n\n{1}% ({2} av {3} MiB lasta ned)", + "tv_xe1": "kunne ikkje laste tekstfil:\n\nfeil ", + "tv_xe2": "404, Fil ikkje funne", + "tv_lst": "tekstfiler i mappa", + "tvt_close": "gå tilbake åt mappa$NSnarvei: M (eller Esc)\">❌ lukk", + "tvt_dl": "last ned denne fila$NSnarvei: Y\">💾 last ned", + "tvt_prev": "vis førre dokument$NSnarvei: i\">⬆ forr.", + "tvt_next": "vis neste dokument$NSnarvei: K\">⬇ neste", + "tvt_sel": "markér fila   ( for utklipp / sletting / ... )$NSnarvei: S\">merk", + "tvt_edit": "redigér fila$NSnarvei: E\">✏️ endre", + "tvt_tail": "overvak fila for endringar og vis nye linjer i sanntid\">📡 følg", + "tvt_wrap": "tekstbryting\">↵", + "tvt_atail": "hald dei nyaste linjene synlege (lås åt botnen av sida)\">⚓", + "tvt_ctail": "skjøn og vis terminalfargar (ansi-sekvensar)\">🌈", + "tvt_ntail": "maksgrense for antal bokstavar som skal visast i vindauget", + + "m3u_add1": "songen blei lagd åt i m3u-spelelista", + "m3u_addn": "{0} songer blei lagde åt i m3u-spelelista", + "m3u_clip": "m3u-spelelista blei kopiert åt utklippstavla\n\nneste steg er å oppretta eit tekstdokument med filnamn som sluttar på .m3u og lime inn spelelista der", + + "gt_vau": "ikkje vis videofiler, berre spel lyden\">🎧", + "gt_msel": "markér filer i staden for å åpne dei; ctrl-klikk filer for å overstyre$N$N<em>når aktiv: dobbelklikk ei fil / mappe for å åpne</em>$N$NSnarvei: S\">markering", + "gt_crop": "skjer ikona slik at dei passar betre\">✂", + "gt_3x": "høgare oppløysing på ikon\">3x", + "gt_zoom": "zoom", + "gt_chop": "trim", + "gt_sort": "sortér", + "gt_name": "namn", + "gt_sz": "størr.", + "gt_ts": "dato", + "gt_ext": "type", + "gt_c1": "redusér makslengde på filnamn", + "gt_c2": "auk makslengde på filnamn", + + "sm_w8": "søker...", + "sm_prev": "søkeresultata er frå eit tidlegare søk:\n ", + "sl_close": "lukk søkeresultat", + "sl_hits": "visar {0} treff", + "sl_moar": "hent fleire", + + "s_sz": "størr.", + "s_dt": "dato", + "s_rd": "sti", + "s_fn": "namn", + "s_ta": "meta", + "s_ua": "up@", + "s_ad": "avns.", + "s_s1": "større enn ↓ MiB", + "s_s2": "mindre enn ↓ MiB", + "s_d1": "nyare enn <dato>", + "s_d2": "eldre enn", + "s_u1": "lasta opp etter", + "s_u2": "og/eller før", + "s_r1": "mappaamn inneheld", + "s_f1": "filnamn inneheld", + "s_t1": "song-info inneheld", + "s_a1": "konkrete eigenskapar", + + "md_eshow": "visar forenkla ", + "md_off": "[📜readme] er skrudd av i [⚙️] -- dokument skjult", + + "badreply": "Ugyldig svar frå serveren", + + "xhr403": "403: Høve nekta\n\nkanskje du blei logga ut? prøv å trykk F5", + "xhr0": "ukjend (enten nettverksproblem eller serverkræsj)", + "cf_ok": "om orsak -- liten tilfeldig kontroll, alt OK\n\nting skal fortsetja om ca. 30 sekund\n\nviss ikkje noko skjer, trykk F5 for å laste sida på nytt", + "tl_xe1": "kunne ikkje hente undermapper:\n\nfeil ", + "tl_xe2": "404: Mappa finnast ikkje", + "fl_xe1": "kunne ikkje hente filer i mappa:\n\nfeil ", + "fl_xe2": "404: Mappa finnast ikkje", + "fd_xe1": "kan ikkje opprette ny mappe:\n\nfeil ", + "fd_xe2": "404: Den overordna mappa finnast ikkje", + "fsm_xe1": "kunne ikkje sende melding:\n\nfeil ", + "fsm_xe2": "404: Den overordna mappa finnast ikkje", + "fu_xe1": "kunne ikkje hente lista med nyleg opplastede filer frå serveren:\n\nfeil ", + "fu_xe2": "404: Fila finnast ikkje??", + + "fz_tar": "ukomprimert gnu-tar arkiv, for linux og mac", + "fz_pax": "ukomprimert pax-tar arkiv, litt treigare", + "fz_targz": "gnu-tar pakket med gzip (nivå 3)$N$NNB: denne er veldig treig;$Nukomprimert tar er betre", + "fz_tarxz": "gnu-tar pakket med xz (nivå 1)$N$NNB: denne er veldig treig;$Nukomprimert tar er betre", + "fz_zip8": "zip med filnamn i utf8 (noko problematisk på windows 7 og eldre)", + "fz_zipd": "zip med filnamn i cp437, for høggamle maskiner", + "fz_zipc": "cp437 med tidlig crc32,$Nfor MS-DOS PKZIP v2.04g (oktober 1993)$N(øker behandlingstid på server)", + + "un_m1": "nedanfor kan du angre / slette filer som du nyleg har lastet opp, eller avbryte ufullstendige opplastinger", + "un_upd": "oppdater", + "un_m4": "eller viss du vil dele nedlastings-lenkene:", + "un_ulist": "vis", + "un_ucopy": "kopiér", + "un_flt": "valgfritt filter:  filnamn / filsti må inneholde", + "un_fclr": "nullstill filter", + "un_derr": 'unpost-sletting feilet:\n', + "un_f5": 'noko gjekk galt, prøv å oppdatere lista eller trykk F5', + "un_uf5": "om orsak, men du må laste sida på nytt (f.eks. ved å trykkje F5 eller CTRL-R) før denne opplastinga kan avbrytast", + "un_nou": 'advarsel: kan ikkje vise ufullstendige opplastingar akkurat no; klikk på oppdater-lenka om litt', + "un_noc": 'advarsel: angring av fullførte opplastingar er deaktivert i serverkonfigurasjonen', + "un_max": "visar dei første 2000 filene (bruk filteret for å snevre inn)", + "un_avail": "{0} nyleg opplasta filer kan slettast
{1} ufullstendige opplastingar kan avbrytast", + "un_m2": "sortert etter opplastingstid; nyaste først:", + "un_no1": "men nei, her var det jaggu ikkje noko som slettast kan", + "un_no2": "men nei, her var det jaggu ingenting som passa overens med filteret", + "un_next": "slett dei neste {0} filene nedanfor", + "un_abrt": "avbryt", + "un_del": "slett", + "un_m3": "hentar lista med nyleg opplasta filer...", + "un_busy": "slettar {0} filer...", + "un_clip": "{0} lenkar kopiert åt utklippstavla", + + "u_https1": "du burde", + "u_https2": "bytte åt https", + "u_https3": "for høgare hastigheit", + "u_ancient": 'nettlesaren din er prehistorisk -- mulig du burde bruke bup i staden for', + "u_nowork": "krev firefox 53+, chrome 57+, eller iOS 11+", + "tail_2old": "krev firefox 105+, chrome 71+, eller iOS 14.5+", + "u_nodrop": 'nettlesaren din er for gamal åt å laste opp filer ved å drage dei inn i vindauget', + "u_notdir": "mottok ikkje mappa!\n\nnettlesaren din er for gamal,\nprøv å drage mappa inn i vindauget i staden for", + "u_uri": "for å laste opp bilder frå andre nettlesarvindauge,\nslipp bildet rett på den store last-opp-knappen", + "u_enpot": 'bytt åt enkelt UI (gir sannsynleg raskere opplasting)', + "u_depot": 'bytt åt snæsent UI (gir sannsynleg treigare opplasting)', + "u_gotpot": 'bytta åt eit enklare UI for å laste opp raskere,\n\ndu kan gjerne bytte tilbake altså!', + "u_pott": "

filer:   {0} ferdig,   {1} feilet,   {2} behandlast,   {3} i kø

", + "u_ever": "dette er den primitive opplastaren; up2k krev minst:
chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", + "u_su2k": 'dette er den primitive opplastaren; up2k er betre', + "u_uput": 'litt raskare (uten sha512)', + "u_ewrite": 'du har ikkje høve til å skrive i denne mappa', + "u_eread": 'du har ikkje høve til å lese i denne mappa', + "u_enoi": 'filsøk er deaktivert i serverkonfigurasjonen', + "u_enoow": "kan ikkje overskrive filer her (Delete-rettigheiten er nødvendig)", + "u_badf": 'Disse {0} filene (av totalt {1}) kan ikkje leses, kanskje pga rettigheitsproblem i filsystemet på datamaskinen din:\n\n', + "u_blankf": 'Disse {0} filene (av totalt {1}) er blanke / uten innhald; ønskjer du å laste dei opp uansett?\n\n', + "u_applef": 'Disse {0} filene (av totalt {1}) er antakeleg uønska;\nTrykk OK/Enter for å HOPPE OVER disse filene,\nTrykk Avbryt/ESC for å LASTE OPP disse filene óg:\n\n', + "u_just1": '\nFunkar kanskje betre viss du berre tar éi fil om gangen', + "u_ff_many": 'Viss du bruker Linux / MacOS / Android, så kan dette antalet filer
kanskje kræsje Firefox! Viss det skjer, så prøv igjen (eller bruk Chrome).', + "u_up_life": "Filene slettast frå serveren {0}\netter at opplastingen er fullført", + "u_asku": 'Laste opp disse {0} filene åt {1}', + "u_unpt": "Du kan angre / slette opplastinga med 🧯 oppe åt venstre", + "u_bigtab": 'Vil no vise {0} filer...\n\nDette kan kræsje nettlesaren din. Fortsette?', + "u_scan": 'Les mappane...', + "u_dirstuck": 'Nettleseren din fekk ikkje høve åt å lese følgande {0} filer/mapper, så dei blir hoppa over:', + "u_etadone": 'Ferdig ({0}, {1} filer)', + "u_etaprep": '(forberedar opplasting)', + "u_hashdone": 'synfaring ferdig', + "u_hashing": 'les', + "u_hs": 'serveren tenkjer...', + "u_started": "filene blir no lasta opp 🚀", + "u_dupdefer": "duplikat; vil bli håndtert åt slutt", + "u_actx": "klikk her for å forhindre tap av
yting ved bytte åt andre vindauge/faner", + "u_fixed": "OK!  Løyste seg 👍", + "u_cuerr": "kunne ikkje laste opp del {0} av {1};\nsikkert greit, fortsetjar\n\nfil: {2}", + "u_cuerr2": "server nekta opplastinga (del {0} av {1});\nprøver igjen senere\n\nfil: {2}\n\nerror ", + "u_ehstmp": "prøver igjen; se mld nederst", + "u_ehsfin": "server nekta forespørselen om å ferdigstille filen; prøver igjen...", + "u_ehssrch": "server nekta forespørselen om å utføre søk; prøver igjen...", + "u_ehsinit": "server nekta forespørselen om å begynne ei ny opplasting; prøver igjen...", + "u_eneths": "eit problem med nettverket gjorde at avtale om opplasting ikkje kunne inngås; prøver igjen...", + "u_enethd": "eit problem med nettverket gjorde at filsjekk ikkje kunne utførast; prøver igjen...", + "u_cbusy": "ventar på klarering frå server etter eit lite nettverksglipp...", + "u_ehsdf": "serveren er full!\n\nprøver igjen regelmessig,\ni tilfelle nokon ryddar litt...", + "u_emtleak1": "uff, det er mulig at nettlesaren din har ei minnelekkasje...\nForeslår", + "u_emtleak2": ' helst at du byttar åt https, eller ', + "u_emtleak3": ' at du ', + "u_emtleakc": 'prøver følgande:\nOpplasting vil gå litt treigare, men det får så vere.\nBeklager bryderiet!\n\nPS: feilen skal vere fikset i chrome v107', + "u_emtleakf": 'prøver følgende:\n\nPS: Firefox fiksar forhåpentligvis feilen ein eller annen gong', + "u_s404": "ikkje funne på serveren", + "u_expl": "forklar", + "u_maxconn": "dei fleste nettlesarar tillet ikkje meir enn 6, men firefox lar deg øke grensen med connections-per-server i about:config", + "u_tu": '

ADVARSEL: turbo er på,  avbrotne opplastingar vil muligens ikkje oppdagast og gjenopptakast; hald musepeikaren over turbo-knappen for meir info

', + "u_ts": '

ADVARSEL: turbo er på,  søkeresultat kan vere feil; hold musepeikaren over turbo-knappen for meir info

', + "u_turbo_c": "turbo er deaktivert i serverkonfigurasjonen", + "u_turbo_g": 'turbo blei deaktivert fordi du ikkje har\nhøve åt å sjå mappeinnhold i dette volumet', + "u_life_cfg": 'slett opplasting etter min (eller timar)', + "u_life_est": 'opplastingen slettast ---', + "u_life_max": 'denne mappa tillet ikkje å \noppbevare filer i meir enn {0}', + "u_unp_ok": 'opplasting kan angrast i {0}', + "u_unp_ng": 'opplasting kan IKKE angrast', + "ue_ro": 'du har ikkje høve åt skriving i denne mappa\n\n', + "ue_nl": 'du er ikkje logga inn', + "ue_la": 'du er logga inn som "{0}"', + "ue_sr": 'du er i filsøk-modus\n\nbytt åt opplasting ved å klikke på forstørringsglaset 🔎 (ved siden av den store FILSØK-knappen) og prøv igjen\n\nsorry', + "ue_ta": 'prøv å last opp igjen, det burde fungere no', + "ue_ab": "den same filen er under opplasting åt ei anna mappe, og den må fullførast der før fila kan lastast opp andre plassar.\n\nDu kan avbryte og gløyme den påbegynte opplastinga ved hjelp av 🧯 oppe åt venstre", + "ur_1uo": "OK: Fila blei lastet opp", + "ur_auo": "OK: Alle {0} filene blei lastet opp", + "ur_1so": "OK: Fila blei funne på serveren", + "ur_aso": "OK: Alle {0} filene blei funne på serveren", + "ur_1un": "Opplasting feila!", + "ur_aun": "Alle {0} opplastingene gjekk feil!", + "ur_1sn": "Fila finnast IKKE på serveren", + "ur_asn": "Fann INGEN av dei {0} filene på serveren", + "ur_um": "Ferdig;\n{0} opplastingar gjekk bra,\n{1} opplastingar gjekk feil", + "ur_sm": "Ferdig;\n{0} filer blei funne,\n{1} filer finnast IKKJE på serveren", + + "lang_set": "passar det å laste sida på nytt?", + }, "rus": { "tt": "Русский", @@ -7560,7 +8190,7 @@ var Ls = { }, }; -var LANGS = ["eng", "nor", "chi", "cze", "deu", "fin", "grc", "ita", "nld", "rus", "spa", "ukr"]; +var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "grc", "ita", "nld", "rus", "spa", "ukr"]; if (window.langmod) langmod(); diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index dc540fb4..6ffd95f6 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -343,6 +343,48 @@ var Ls = { "af1": "Recent geüploade bestanden weergeven", "ag1": "Bekende IdP-gebruikers weergeven", }, + "nno": { + "a1": "oppdatér", + "b1": "heisann   (du er ikkje logga inn)", + "c1": "logg ut", + "d1": "tilstand", + "d2": "vis tilstanden åt alle trådar", + "e1": "last innst.", + "e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde", + "f1": "du kan sjå på:", + "g1": "du kan laste opp åt:", + "cc1": "brytarar og slikt:", + "h1": "skru av k304", + "i1": "skru på k304", + "j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig", + "k1": "nullstill innstillinger", + "l1": "logg inn:", + "m1": "velkomen attende,", + "n1": "404: filen finnast ikkje  ┐( ´ -`)┌", + "o1": 'eller kanskje du ikkje har høve? prøv eit passord eller gå heim', + "p1": "403: tilgang nektet  ~┻━┻", + "q1": 'prøv eit passord eller gå heim', + "r1": "gå heim", + ".s1": "kartlegg", + "t1": "handling", + "u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund", + "v1": "kople åt", + "v2": "bruk denne serveren som ein lokal harddisk", + "w1": "bytt åt https", + "x1": "bytt passord", + "y1": "dine delinger", + "z1": "lås opp område:", + "ta1": "du må skrive eit nytt passord først", + "ta2": "gjenta for å stadfeste nytt passord:", + "ta3": "fant ein skrivefeil; vennligst prøv igjen", + "aa1": "innkommande:", + "ab1": "skru av no304", + "ac1": "skru på no304", + "ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!", + "ae1": "utgående:", + "af1": "vis nylig opplasta filer", + "ag1": "vis kjente IdP-brukarar", + }, "spa": { "a1": "actualizar", "b1": "hola   (no has iniciado sesión)", From 392a4db55bc4d27300bff5bc82638513e6c900d3 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sat, 9 Aug 2025 00:55:57 +0200 Subject: [PATCH 002/154] Add Polish translation (#463) Add Polish translation --------- Signed-off-by: Artur Borecki Signed-off-by: ed Co-authored-by: dai Co-authored-by: ed --- copyparty/web/browser.js | 626 +++++++++++++++++++++++++++++++++++++++ copyparty/web/splash.js | 42 +++ 2 files changed, 668 insertions(+) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 70ee0acf..b3d174f8 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -6302,6 +6302,632 @@ var Ls = { "lang_set": "passar det å laste sida på nytt?", }, + "pol": { + "tt": "Polski", + "cols": { + "c": "przyciski akcji", + "dur": "czas trwania", + "q": "jakość / bitrate", + "Ac": "kodek audio", + "Vc": "kodek wideo", + "Fmt": "format / kontener", + "Ahash": "suma kontrolna audio", + "Vhash": "suma kontrolna wideo", + "Res": "rozdzielczość", + "T": "rodzaj pliku", + "aq": "jakość / bitrate audio", + "vq": "jakość / bitrate wideo", + "pixfmt": "podpróbkowanie / struktura pikseli", + "resw": "rozdzielczość pozioma", + "resh": "rozdzielczość pionowa", + "chs": "kanały audio", + "hz": "częstotliwość próbkowania" + }, + + "hks": [ + [ + "misc", + ["ESC", "zamknij różne rzeczy"], + + "file-manager", + ["G", "przełącz widok lista / siatka"], + ["T", "przełącz miniaturki / ikony"], + ["⇧ A/D", "wielkość miniaturki"], + ["ctrl-K", "usuń zaznaczone"], + ["ctrl-X", "wytnij zaznaczone do schowka"], + ["ctrl-C", "skopiuj zaznaczone do schowka"], + ["ctrl-V", "wklej (przenieś/skopiuj) tutaj"], + ["Y", "pobierz zaznaczone"], + ["F2", "zmień nazwę zaznaczonych"], + + "file-list-sel", + ["spacja", "przełącz zaznaczanie plików"], + ["↑/↓", "przenieś kursor zaznaczenia"], + ["ctrl ↑/↓", "przenieś kursor i widok"], + ["⇧ ↑/↓", "wybierz poprzedni/następny plik"], + ["ctrl-A", "wybierz wszystkie pliki/foldery"], + ], [ + "navigation", + ["B", "przełącz ścieżkę nawigacyjną / panel nawigacyjny"], + ["I/K", "poprzedni/następny folder"], + ["M", "folder nadrzędny (lub zwiń aktualny)"], + ["V", "przełącz foldery / pliki tekstowe w panelu nawigacyjnym"], + ["A/D", "rozmiar panelu nawigacyjnego"], + ], [ + "audio-player", + ["J/L", "poprzedni/następny utwór"], + ["U/O", "przejdź 10 sek. do tyłu/przodu"], + ["0..9", "przeskocz do 0%..90%"], + ["P", "odtwórz/pauza (również rozpoczyna)"], + ["S", "wybierz odtwarzany utwór"], + ["Y", "pobierz utwór"], + ], [ + "image-viewer", + ["J/L, ←/→", "poprzednie/następne zdjęcie"], + ["Home/End", "pierwsze/ostatnie zdjęcie"], + ["F", "pełny ekran"], + ["R", "obróć zgodnie ze wskaz. zegara"], + ["⇧ R", "obróć przeciwnie do ruchu wskaz. zegara"], + ["S", "wybierz zdjęcie"], + ["Y", "pobierz zdjęcie"], + ], [ + "video-player", + ["U/O", "przejdź 10 sek. do tyłu/przodu"], + ["P/K/Spacja", "odtwórz/pauza"], + ["C", "odtwarzaj następne po zakończeniu"], + ["V", "odtwarzaj w pętli"], + ["M", "wycisz"], + ["[ i ]", "ustaw opóźnienie pętli"], + ], [ + "textfile-viewer", + ["I/K", "poprzedni/następny plik"], + ["M", "zamknij plik"], + ["E", "edytuj plik"], + ["S", "wybierz plik (do wycięcia/skopiowania/zmiany nazwy)"], + ] + ], + + "m_ok": "OK", + "m_ng": "Anuluj", + + "enable": "Włącz", + "danger": "NIEBEZPIECZEŃSTWO", + "clipped": "skopiowano do schowka", + + "ht_s1": "sekunda", + "ht_s2": "sekund(y)", + "ht_m1": "minuta", + "ht_m2": "minuty", + "ht_h1": "godzina", + "ht_h2": "godziny", + "ht_d1": "dzień", + "ht_d2": "dni", + "ht_and": " i ", + + "goh": "panel sterowania", + "gop": 'poprzedni plik/folder">poprzedni', + "gou": 'nadrzędny folder">w górę', + "gon": 'następny folder">następny', + "logout": "Wyloguj ", + "access": " dostęp", + "ot_close": "zamknij pod-menu", + "ot_search": "szukaj plików po atrybutach, ścieżce / nazwie, tagach muzyki, bądź dowolnej ich kombinacji$N$N<code>foo bar</code> = musi zawierać «foo» oraz «bar»,$N<code>foo -bar</code> = musi zawierać «foo», lecz nie «bar»,$N<code>^yana .opus$</code> = musi zaczynać się od «yana» i być plikiem «opus»$N<code>"try unite"</code> = zawierać dokładnie «try unite»$N$Nformatem daty jest iso-8601, czyli$N<code>2009-12-31</code> lub <code>2020-09-12 23:30:00</code>", + "ot_unpost": "unpost: usuń ostatnio przesłane pliki lub przerwij przesyłanie", + "ot_bup": "bup: podstawowe przesyłanie danych, wspiera nawet netscape 4.0", + "ot_mkdir": "mkdir: tworzy nowy folder", + "ot_md": "new-md: tworzy nowy dokument markdown", + "ot_msg": "msg: wysyła wiadomość do loga serwera", + "ot_mp": "opcje odtwarzacza multimediów", + "ot_cfg": "opcje konfiguracji", + "ot_u2i": 'up2k: przesyła pliki (jeżeli masz dostęp do zapisu) lub uruchomia tryb wyszukiwania, aby sprawdzić czy już istnieją na serwerze$N$Nprzesyłanie można wznowić, jest wielowątkowe i znaczniki czasu są zachowywane, lecz zużywa więcej procesora niż [🎈]  (podstawowe przesyłanie)

podczas przesyłania ta ikona zamienia się w wskaźnik postępu!', + "ot_u2w": 'up2k: przesyła pliki z możliwością wznowienia (można zamknąć przeglądarkę i dokończyć przesyłanie plików później)$N$Njest wielowątkowy i zachowuje znaczniki czasu plików, lecz zużywa więcej procesora od [🎈]  (podstawowego przesyłania)

podczas przesyłania ta ikona zamienia się w wskaźnik postępu!', + "ot_noie": 'Użyj przeglądarki Chrome / Firefox / Edge', + + "ab_mkdir": "stwórz folder", + "ab_mkdoc": "stwórz dok. markdown", + "ab_msg": "wyślij wiad. do logów serwera", + + "ay_path": "przejdź do folderów", + "ay_files": "przejdź do plików", + + "wt_ren": "zmień nazwę zaznaczonych elementów$NSkrót: F2", + "wt_del": "usuń zaznaczone elementy$NSkrót: ctrl-K", + "wt_cut": "wytnij zaznaczone elementy <small>(aby wkleić gdzie indziej)</small>$NSkrót: ctrl-X", + "wt_cpy": "skopiuj zaznaczone pliki do schowka$N(aby wkleić gdzie indziej)$NSkrót: ctrl-C", + "wt_pst": "wklej wcześniej wycięte/skopiowane zaznaczenie$NSkrót: ctrl-V", + "wt_selall": "zaznacz wszystko$NHotkey: ctrl-A (when file focused)", + "wt_selinv": "odwróć zaznaczenie", + "wt_zip1": "pobierz folder jako archiwum", + "wt_selzip": "pobierz zaznaczone jako archiwum", + "wt_seldl": "pobierz zaznaczenie jako oddzielne pliki$NSkrót: Y", + "wt_npirc": "skopiuj informacje o utworze w formacie irc", + "wt_nptxt": "skopiuj informacje o utworze jako zwykły tekst", + "wt_m3ua": "dodaj to playlisty m3u (kliknij 📻copy kliknij)", + "wt_m3uc": "skopiuj playlistę m3u do schowka", + "wt_grid": "przełącz widok siatki / listy$NSkrót: G", + "wt_prev": "poprzeni utwór$NSkrót: J", + "wt_play": "odtwórz / pauza$NSkrót: P", + "wt_next": "następny utwór$NSkrót: L", + + "ul_par": "przesyłane równolegle:", + "ut_rand": "losuj nazwy plików", + "ut_u2ts": "kopiuj znacznik ostatniej modyfikacji$Nz twojego systemu plików na serwer\">📅", + "ut_ow": "nadpisywać istniejące pliki na serwerzę?$N🛡️: nigdy (wygeneruje nową nazwę)$N🕒: nadpisz jeśli pliki na serwerze są starsze niż przesyłane$N♻️: zawsze nadpisuj jeśli zawartość plików się różni", + "ut_mt": "hashuj inne pliki podczas przesyłania$N$Nmożna wyłączyć w przypadku wystąpienia wąskiego gardła na CPU lub HDD", + "ut_ask": 'pytaj o potwierdzenie rozpoczęcia przesyłania">💭', + "ut_pot": "przyspiesz przesyłanie na słabszych urządzeniach,$Nupraszczając interfejs", + "ut_srch": "nie przesyłaj plików, jedynie sprawdź czy istnieją$Njuż na serwerze (przeskanuje wszystkie foldery dostępne do odczytu)", + "ut_par": "zatrzymuje przesyłanie jeśli wynosi 0$N$Nzwiększ w przypadku jeśli twoja sieć jest wolna / ma duże opóźnienia$N$Nustaw wartość 1 w sieci lokalnej lub w przypadku wolnego dysku serwerowego", + "ul_btn": "upuść pliki / foldery
tutaj (lub kliknij mnie)", + "ul_btnu": "P R Z E Ś L I J", + "ul_btns": "S Z U K A J", + + "ul_hash": "hashowanie", + "ul_send": "przesyłanie", + "ul_done": "gotowe", + "ul_idle1": "nic się jeszcze nie przesyła", + "ut_etah": "średnia prędkość <em>hashowania</em> i przewidywany czas do końca", + "ut_etau": "średnia prędkość <em>przesyłania</em> i przewidywany czas do końca", + "ut_etat": "średnia prędkość <em>ogólna</em> i przewidywany czas do końca", + + "uct_ok": "zakończone pomyślnie", + "uct_ng": "zakończono niepowodzeniem (odrzucono, nie znaleziono, itp.)", + "uct_done": "zakończono z błędami", + "uct_bz": "w trakcie (oblicznie sumy kontrolnej, przesyłanie)", + "uct_q": "oczekujące", + + "utl_name": "nazwa pliku", + "utl_ulist": "lista", + "utl_ucopy": "kopia", + "utl_links": "linki", + "utl_stat": "status", + "utl_prog": "postęp", + + // keep short: + "utl_404": "404", + "utl_err": "BŁĄD", + "utl_oserr": "błąd OS", + "utl_found": "znaleziono", + "utl_defer": "opóźnij", + "utl_yolo": "YOLO", + "utl_done": "gotowe", + + "ul_flagblk": "pliki zostały zakolejkowane,
lecz przesyłanie up2k już trwa (w innej zakładce),
oczekuję na zakończenie", + "ul_btnlk": "przełącznik zablokowany przez konfigurację serwera", + + "udt_up": "Prześlij", + "udt_srch": "Szukaj", + "udt_drop": "upuść tutaj", + + "u_nav_m": '
co my tu mamy?
Enter = Pliki (jeden lub wiecej)\nESC = Jeden folder (włącznie z podfolderami)', + "u_nav_b": 'PlikiJeden folder', + + "cl_opts": "przełączniki", + "cl_themes": "motyw", + "cl_langs": "język", + "cl_ziptype": "pobieranie folderów", + "cl_uopts": "przełączniki przesyłania (up2k)", + "cl_favico": "favicon (ikona w przeglądarce)", + "cl_bigdir": "duże foldery", + "cl_hsort": "#sortowanie", + "cl_keytype": "notacja klucza", // not sure + "cl_hiddenc": "ukryte kolumny", + "cl_hidec": "ukryj", + "cl_reset": "zresetuj", + "cl_hpick": "kliknij nagłówki kolumn, aby ukryć je w tabeli niżej", + "cl_hcancel": "ukrywanie kolumn przerwane", + + "ct_grid": '田 siatka', + "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_ihop": 'przejdź do ostatniego pliku po zamknięciu przeglądarki obrazów">g⮯', + "ct_dots": 'pokaż ukryte pliki (jeśli pozwala serwer)">ukryte', + "ct_qdel": 'pytaj o potwierdzenie przy usuwaniu tylko raz">pyt. us.', + "ct_dir1st": 'pokazuj foldery na początku">📁 najpierw', + "ct_nsort": 'naturalne sortowanie (dla numerowanych plików)">nsort', + "ct_utc": 'pokaż wszystkie daty/czas w UTC">UTC', + "ct_readme": 'pokazuj README.md w folderach">📜 readme', + "ct_idxh": 'pokazuj plik index.html zamiast zawartości folderu">htm', + "ct_sbars": 'pokazuj paski przewijania">⟊', + + "cut_umod": "uaktualnij znacznik ostatniej modyfikacji pliku, tak aby pasował do pliku lokalnego jeżeli plik już istnieje na serwerze (wymaga dostępu zapisu i usuwania)\">📅 ponownie", + + "cut_turbo": "przycisk „raz się żyje”, raczej NIE POWINIENEŚ tego włączać:$N$Nużywaj jeśli przesyłano ogromną liczbę plików i z jakiegoś powodu musisz przesłać pliki ponownie, kontynuując jak najszybciej$N$Nzamienia sprawdzanie sumy kontrolnej plików prostym "czy ten plik jest tego samego rozmiaru jak ten na serwerze?" więc jeśli pliki różnią się zawartością, ale są tego samego rozmiaru, NIE ZOSTANĄ przesłane ponownie$N$Nta opcja powinna zostać wyłączona po zakończeniu przesyłania, i potem "przesłać" te same pliki ponownie w celu weryfikacji\">turbo", + + "cut_datechk": "przy wyłączonym przycisku turbo nic nie robi$N$Nleciutko zmniejsza czynnik „raz się żyje”; dodatkowo sprawdza czy znaczniki modyfikacji pliku przesyłanego zgadzają się z serwerem$N$Nteorytycznie powinno złapać to większość niedokończonych / uszkodzonych plików, lecz nie jest zamiennikiem wykonania ponownego sprawdzenia bez włączonego trybu turbo\">spr-daty", + + "cut_u2sz": "rozmiar (w MiB) każdego kawałka do przesłania; większe wartości szybciej latają po Atlantyku. Mniejsze wartości działają lepiej na bardzo niestabilnych połączeniach (neostrada?)", + + "cut_flag": "zapewnia, że tylko jedna karta przesyła dane w danym momencie$N -- opcja musi być włączona na innych kartach $N - dotyczy tylko kart w tej samej domenie", + + "cut_az": "przesyła pliki w kolejności alfabetycznej, zamiast rozpocząć od najmniejszego pliku$N$Nkolejność alfabetyczna może ułatwić oszacowanie, co mogło pójść nie tak na serwerze, lecz lekko spowalnia przesyłanie po światłowodzie lub w sieci lokalnej", + + "cut_nag": "powiadomienie systemowe po zakończeniu przesyłania$N(tylko jeśli przeglądarka lub karta nie jest aktywna)", + "cut_sfx": "sygnał dźwiękowy po zakończeniu przesyłania$N(tylko jeśli przeklądarka lub karta nie jest aktywna)", + + "cut_mt": "używaj wielowątkowości, aby przyspieszyć obliczanie sumy kontrolnej plików$N$Nużywa web workerów i wymaga$Nwięcej pamięci RAM (do 512 MiB)$N$Nprzyspiesza https o 30% i http 4,5-krotnie\">ww", + + "cut_wasm": "używaj WASM zamiast wbudowanego hashera przeglądarki; zwiększa prędkość na Chrome'o-pochodnych przeglądarkach, zwiększając zużycie procesora, ponadto wiele starszych wersji Chrome'a zawiera błędy powodujące zeżarcie całej pamięci RAM komputera i przymusowe zamknięcie przeglądarki jeżeli ta opcja jest włączona\">wasm", + + "cft_text": "tekst favicon (aby wyłączyć, usuń zawartość i przeładuj stronę)", + "cft_fg": "kolor tekstu", + "cft_bg": "kolor tła", + + "cdt_lim": "maksymalna liczba plików do pokazania na raz w folderze", + "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", + + "tt_entree": "pokaż panel nawigacyjny (panel boczny z drzewem folderów)$NSkrót: B", + "tt_detree": "pokaż ślad nawigacyjny$NSkrót: B", + "tt_visdir": "przewiń do wybranego folderu", + "tt_ftree": "przełącz drzewo folderów / pliki tekstowe$NSkrót: V", + "tt_pdock": "pokaż foldery nadrzędne w przypiętym u góry panelu", + "tt_dynt": "rozszerzaj panel wraz z drzewem", + "tt_wrap": "zawijaj tekst", + "tt_hover": "pokazuj za długie linie po najechaniu kursorem$N( psuje przewijanie gdy $N  kursor nie jest w lewym marginesie )", + + "ml_pmode": "na końcu folderu...", + "ml_btns": "komendy", + "ml_tcode": "transkoduj", + "ml_tcode2": "transkoduj do", + "ml_tint": "odcień", + "ml_eq": "korektor dźwięku (equalizer)", + "ml_drc": "kompresor zasięgu dynamiki", + + "mt_loop": "pętla/powtarzaj jeden utwór\">🔁", + "mt_one": "zatrzymaj po jednym utworze\">1️⃣", + "mt_shuf": "odtwarzaj losowo w każdym folderze\">🔀", + "mt_aplay": "autoodtwarzanie po kliknięciu linku do tego serwera, zawierającego identyfikator utworu$N$Nwyłączenie tej opcji zapobiegnie aktualizowaniu adresu strony podczas odtwarzania muzyki, aby zapobiec autoodtwarzaniu przy utracie ustawień\">a▶", + "mt_preload": "rozpocznij ładowanie kolejnego utworu blisko końca aktualnego w celu uzyskania odtwarzania bez przerw\">preload", + "mt_prescan": "przechodzi do następnego folderu przed zakończeniem ostatniego utworu,$Naby udobruchać przeglądarkę,$Nżeby nie zatrzymała odtwarzania\">naw", + "mt_fullpre": "próbuj zbuforować cały utwór;$N✅ włącz na niestabilnych połączeniach,$N❌ wyłącz na wolnych połączeniach\">pełnebuf", + "mt_fau": "nie zatrzymuj muzyki jeśli następna piosenka będzie się zbyt wolno buforować na telefonach (może sprawić, że tagi będą się niepoprawnie wyświetlać)\">☕️", + "mt_waves": "falisty pasek:$Npokazuj amplitudę dźwięku w pasku utworu\">~s", + "mt_npclip": "pokaż przyciski kopiowania aktualnie odtwarzanego utworu\">/np", + "mt_m3u_c": "pokaż przyciski kopiowania$Nwybranych piosenek jako playlista m3u8\">📻", + "mt_octl": "integracja z systemem operacyjnym (przyciski multimedialne / informacje o utworze)\">os-int", + "mt_oseek": "zezwól na przewijanie utworu poprzez integrację z systemem$N$Nuwaga: na niektórych urządzeniach (iPhone'y),$Nzamienia przycisk następnej piosenki\">seek", + "mt_oscv": "pokaż okładkę albumu w widoku systemu\">okładka", + "mt_follow": "podążaj za odtwarzanym utworem przewijając widok\">🎯", + "mt_compact": "kompaktowe sterowanie\">⟎", + "mt_uncache": "wyczyść pamięć podręczną  (spróbuj jeśli przeglądarka$Nzachowała zepsutą kopię utworu, przez co nie odtwarza się ona)\">uncache", + "mt_mloop": "odtwarzaj utwory w folderze w pętli\">🔁 loop", + "mt_mnext": "wczytaj następny folder i kontynuuj\">📂 next", + "mt_mstop": "zatrzymaj odtwarzanie\">⏸ stop", + "mt_cflac": "przekonwertuj format flac / wav na opus\">flac", + "mt_caac": "przekonwertuj format aac / m4a na opus\">aac", + "mt_coth": "przekonwertuj wszystkie inne formaty (nie będące mp3) na opus\">oth", + "mt_c2opus": "najlepszy wybór dla komputerów, laptopów i urządzeń z androidem\">opus", + "mt_c2owa": "opus-weba, dla iOS 17.5 i nowszych\">owa", + "mt_c2caf": "opus-caf, dla iOS 11 do 17\">caf", + "mt_c2mp3": "używaj na bardzo starych urządzeniach\">mp3", + "mt_c2ok": "cudownie, dobry wybór", + "mt_c2nd": "ten format nie jest rekomendowany dla twojego urządzenia, ale nadal jest w porządku", + "mt_c2ng": "wygląda na to, że to urządzenie nie wspiera tego formatu, lecz spróbujmy i tak", + "mt_xowa": "iOS zawiera błędy uniemożliwiające odtwarzanie w tle używając tego formatu; wybierz caf lub mp3", + "mt_tint": "jasność tła (0-100) paska,$Naby zmniejszyć widoczność buforowania", + "mt_eq": "włącza korektor dźwięku (equalizer) i kontrolę wzmocnienia dźwięku;$N$Nboost <code>0</code> = standardowa głośność 100% (niezmodyfikowana)$N$Nwidth <code>1  </code> = standardowe stereo (niezmodyfikowane)$Nwidth <code>0.5</code> = 50% crossfeed lewo-prawo$Nwidth <code>0  </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = usuwanie wokalu :^)$N$Nwłączenie korektora sprawia, że albumy bezprzerwowe są w pełni bez przerw, więc jeśli jest to dla ciebie ważne, zostaw wszystko na 0 (poza width = 1)", + "mt_drc": "włącza kompresor zakresu dynamiki (normalizacja głośności); włącza również korektor w celu zbalansowania tego spaghetti, więc ustaw wszystkie opcje korektora, oprócz 'width',na 0, jeśli go nie chcesz$N$Nobniża głośność audio nad THRESHOLD (próg) dB; dla każdego RATIO (współczynnika) dB, będącego ponad THRESHOLDem jest 1 dB wyjścia, więc domyślne wartości progu -24 i współczynnika 12 znaczą, że nigdy nie powinno być głośniej niż -22 dB i bezpieczne jest zwiększenie wzmocnienia korektora do 0.8, lub nawet 1.8 z ATK 0 i ogromnym RLS, jak 90 (działa tylko na firefoxie, inne przeglądarki mają limit RLS 1)$N$N(na wikipedii tłumaczą to dużo lepiej)", + + "mb_play": "odtwórz", + "mm_hashplay": "odtworzyć ten plik audio?", + "mm_m3u": "naciśnij Enter/OK, aby odtworzyć\nnaciśnij ESC/Cancel, aby edytować", + "mp_breq": "wymagany jest Firefox 82+, Chrome 73+ lub iOS 15+", + "mm_bload": "wczytywanie...", + "mm_bconv": "konwertowanie do {0}, proszę czekać...", + "mm_opusen": "ta przeglądarka nie może odtwarzać plików aac / m4a;\ntranskodowanie do formatu opus włączone", + "mm_playerr": "odtwarzanie nie powiodło się: ", + "mm_eabrt": "Odtwarzanie zostało przerwane", + "mm_enet": "Połączenie z internetem jest słabe", + "mm_edec": "Ten plik wydaje się uszkodzony??", + "mm_esupp": "Twoja przeglądarka nie rozumie tego formatu audio", + "mm_eunk": "Nieznany błąd", + "mm_e404": "Nie można odtworzyć; błąd 404: Nie znaleziono pliku.", + "mm_e403": "Nie można odtworzyć; błąd 403: Odmowa dostępu.\n\nSpróbuj przeładować stronę (F5), może cię wylogowało", + "mm_e500": "Nie można odtworzyć; błąd 500: Sprawdź logi serwera.", + "mm_e5xx": "Nie można odtworzyć; błąd serwera", + "mm_nof": "nie znaleziono więcej plików audio", + "mm_prescan": "Szukanie kolejnego utworu...", + "mm_scank": "Znaleziono następną piosenkę:", + "mm_uncache": "wyczyszczono pamięć podręczną; wszystkie utwory zostaną pobrane ponownie przy następnym odtworzeniu", + "mm_hnf": "ten utwór już nie istnieje", + + "im_hnf": "ten obraz już nie istnieje", + + "f_empty": 'ten folder jest pusty', + "f_chide": 'schowa kolumnę «{0}»\n\nkolumny można ponownie pokazać w zakładce ustwaień', + "f_bigtxt": "ten plik waży {0} MiB -- na pewno pokazać jako tekst?", + "f_bigtxt2": "odczytać jedynie koniec pliku? włączy również śledzenie, pokazując nowo-dodane linie tekstu w czasie rzeczywistym", + "fbd_more": '
pokazuję {0} z {1} plików; pokaż {2} lub pokaż wszystko
', + "fbd_all": '
pokazuję {0} z {1} files; pokaż wszystko
', + "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_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", + + "ft_paste": "wklej {0} elementów$NSkrót: ctrl-V", + "fr_eperm": 'nie można zmienić nazwy:\nnie posiadasz uprawnienia „move” w tym folderze', + "fd_eperm": 'nie można usunąć:\nnie posiadasz uprawnienia „delete” w tym folderze', + "fc_eperm": 'nie można wyciąć:\nnie posiadasz uprawnienia „move” w tym folderze', + "fp_eperm": 'nie można wkleić:\nnie posiadasz uprawnienia „write” w tym folderze', + "fr_emore": "wybierz przynajmniej jeden element do zmiany nazwy", + "fd_emore": "wybierz przynajmniej jeden element do usunięcia", + "fc_emore": "wybierz przynajmniej jeden element do wycięcia", + "fcp_emore": "wybierz przynajmniej jeden element do skopiowania", + + "fs_sc": "udostępnij ten folder", + "fs_ss": "udostępnij zaznaczone pliki", + "fs_just1d": "nie można wybrać więcej niż jednego folderu,\nani mieszać plików i folderów w jednym zaznaczeniu", + "fs_abrt": "❌ przerwij", + "fs_rand": "🎲 losuj nazwę", + "fs_go": "✅ stwórz udostępnienie", + "fs_name": "nazwa", + "fs_src": "źródło", + "fs_pwd": "hasło", + "fs_exp": "wygaśnięcie", + "fs_tmin": "min", + "fs_thrs": "godz.", + "fs_tdays": "dni", + "fs_never": "na zawsze", + "fs_pname": "opcjonalna nazwa linku; zostanie wylosowana jeśli pusta", + "fs_tsrc": "plik lub folder do udostępnienia", + "fs_ppwd": "hasło (opcjonalnie)", + "fs_w8": "udostępnianie...", + "fs_ok": "naciśnij Enter/OK, aby skopiować do schowka\nnaciśnij ESC/Anuluj, aby zamknąć", + + "frt_dec": "może naprawić niektóre zepsute nazwy plików\">dekoduj-url", + "frt_rst": "zresetuj zmodyfikowane nazwy plików do oryginalnych\">↺ zresetuj", + "frt_abrt": "przerwij i zamknij to okno\">❌ anuluj", + "frb_apply": "ZASTOSUJ ZMIANĘ NAZWY", + "fr_adv": "zmiana nazwy hurtowa / metadanych / wzorcem\">zaawansowane", + "fr_case": "rozróżnianie wielkości liter w regex\">wlit", + "fr_win": "nazwy bezpieczne dla systemu Windows; zamienia symbole <>:"\\|?* na japońskie odpowiedniki\">win", + "fr_slash": "zamienia / symbolem, który nie tworzy nowych folderów\">brak /", + "fr_re": "wzorzec wyszukiwania regex stosowany do oryginalnych nazw plików; do grup przechwytywania można się odwołać w polu formatu poniżej, np. <code>(1)</code> i <code>(2)</code> itd.", + "fr_fmt": "inspirowane programem foobar2000:$N<code>(title)</code> zostaje zamienione na tytuł utworu,$N<code>[(artist) - ](title)</code> pomija [tą] część jeśli pole artysty jest puste$N<code>$lpad((tn),2,0)</code> wyrównuje numer utworu do 2 cyfr (np. 01, 06, 09, 16)", + "fr_pdel": "usuń", + "fr_pnew": "zapisz jako", + "fr_pname": "podaj nazwę nowego szablonu", + "fr_aborted": "anulowano", + "fr_lold": "poprzednia nazwa", + "fr_lnew": "nowa nazwa", + "fr_tags": "znaczniki dla wybranych plików (tylko do odczytu, w celach informacyjnych):", + "fr_busy": "zmienianie nazwy {0} plików...\n\n{1}", + "fr_efail": "zmiana nazwy zakończona niepowodzeniem:\n", + "fr_nchg": "{0} nowych nazw zostało zmienionych przez opcje win i/lub brak /\n\nKontynuować ze zmienionymi nazwami?", + + "fd_ok": "usunięto", + "fd_err": "usuwanie zakończone niepowodzeniem:\n", + "fd_none": "nie usunięto nic; usunięcie mogło zostać zablokowane przez konfigurację serwera (xbd)?", + "fd_busy": "usuwanie {0} elementów...\n\n{1}", + "fd_warn1": "USUNĄĆ {0} elementów?", + "fd_warn2": "OSTATNIA SZANSA! Tej operacji nie da się cofnąć. Usunąć?", + + "fc_ok": "wycięto {0} elementów", + "fc_warn": 'wycięto {0} elementów,\n\nlecz można je wkleić tylko w tej karcie\n(ze względu na ogromną ilość wybranych elementów)', + + "fcc_ok": "skopiowano {0} elementów do schowka", + "fcc_warn": 'skopiowano {0} elementów,\n\nlecz można je wkleić tylko w tej karcie\n(ze względu na ogromną ilość wybranych elementów)', + + "fp_apply": "zastosuj te nazwy", + "fp_ecut": "najpierw wytnij lub skopiuj pliki / foldery, aby je wkleić / przenieść\n\nuwaga: można wycinać / wklejać pomiędzy różnymi kartami przeglądarki", + "fp_ename": "Nie udało się przenieść {0} elementów, gdyż ich nazwy już istnieją w tym folderze. Nadaj im nowe nazwy poniżej, bądź zostaw pole nazwy puste, aby je pominąć:", + "fcp_ename": "Nie udało się przekopiować {0} elementów, gdyż ich nazwy już istnieją w tym folderze. Nadaj im nowe nazwy poniżej, bądź zostaw pole nazwy puste, aby je pominąć:", + "fp_emore": "pozostało jeszcze kilka kolizji nazw plików do poprawy", + "fp_ok": "przeniesiono", + "fcp_ok": "przekopiowano", + "fp_busy": "przenoszenie {0} elementów...\n\n{1}", + "fcp_busy": "kopiowanie {0} elementów...\n\n{1}", + "fp_err": "nie udało się przenieść:\n", + "fcp_err": "nie udało się skopiować:\n", + "fp_confirm": "przenieść tutaj {0} elementy(ów)?", + "fcp_confirm": "skopiować tutaj {0} elementy(ów)?", + "fp_etab": 'nie udało się odczytać schowka z innej karty przeglądarki', + "fp_name": "przesyłanie pliku z twojego urządzenia. Nadaj nazwę:", + "fp_both_m": '
wybierz metodę wklejenia
Enter = Przenieś {0} pliki(ów) z «{1}»\nESC = Prześlij {2} pliki(ów) z twojego urządzenia', + "fcp_both_m": '
wybierz metodę wklejenia
Enter = Skopiuj {0} pliki(ów) z «{1}»\nESC = Prześlij {2} pliki(ów) z twojego urządzenia', + "fp_both_b": 'PrzenieśPrześlij', + "fcp_both_b": 'KopiujPrześlij', + + "mk_noname": "wpisz nazwę do pola po lewej zanim to zrobisz :p", + + "tv_load": "Wczytywanie pliku tekstowego:\n\n{0}\n\n{1}% (wczytano {2} z {3} MiB)", + "tv_xe1": "nie udało się wczytać pliku:\n\nbłąd ", + "tv_xe2": "404, nie znaleziono pliku", + "tv_lst": "lista plików tekstowych w", + "tvt_close": "powróć do widoku folderów$NSkrót: M (lub Esc)\">❌ zamknij", + "tvt_dl": "pobierz ten plik$NHotkey: Y\">💾 pobierz", + "tvt_prev": "pokaż poprzedni dokument$NSkrót: i\">⬆ poprzedni", + "tvt_next": "pokaż następny dokument$NSkrót: K\">⬇ następny", + "tvt_sel": "wybierz plik   ( do wycięcia / skopiowania / usunięcia / itp. )$NSkrót: S\">wyb", + "tvt_edit": "otwórz plik w edytorze tekstu$NSkrót: E\">✏️ edytuj", + "tvt_tail": "śledź zmiany w pliku; pokazuj nowe linie w czasie rzeczywistym\">📡 śledź", + "tvt_wrap": "zawijaj tekst\">↵", + "tvt_atail": "utrzymuj widok na dole strony\">⚓", + "tvt_ctail": "dekoduj kolory terminala (sekwencje sterujące ANSI)\">🌈", + "tvt_ntail": "limit przewijania (ile bajtów tekstu przechowywać w pamięci)", + + "m3u_add1": "dodano utwór do playlisty m3u", + "m3u_addn": "dodano {0} utwory(ów) do playlisty m3u", + "m3u_clip": "skopiowano playlistę m3u do schowka\n\nutwórz", + + "gt_vau": "nie pokazuj obrazu, odtwarzaj tylko dźwięk\">🎧", + "gt_msel": "wybierz pliki; kliknij plik z wciśniętym klawiszem CTRL, aby zastąpić$N$N<em>gdy tryb jest aktywny, kliknij dwukrotnie na plik / folder, żeby go otworzyć</em>$N$NSkrót: S\">wybierz wiele", + "gt_crop": "kadruj miniaturki do środka\">kadruj", + "gt_3x": "miniaturki w wysokiej rozdzielczości\">3x", + "gt_zoom": "przybliż", + "gt_chop": "przytnij", + "gt_sort": "sortuj według", + "gt_name": "nazwa", + "gt_sz": "rozmiar", + "gt_ts": "data", + "gt_ext": "typ", + "gt_c1": "przycinaj większą część nazw plików (pokazuj mniej)", + "gt_c2": "przycinaj mniejszą część nazw plików (pokazuj więcej)", + + "sm_w8": "wyszukiwanie...", + "sm_prev": "wyniki wyszukiwania poniżej pochodzą z poprzedniego zapytania:\n ", + "sl_close": "zamknij wyniki wyszukiwania", + "sl_hits": "pokazuję {0} wyniki(ów)", + "sl_moar": "pokaż więcej", + + "s_sz": "rozmiar", + "s_dt": "data", + "s_rd": "ścieżka", + "s_fn": "nazwa", + "s_ta": "znaczniki", + "s_ua": "data przesłania", + "s_ad": "zaawansowane", + "s_s1": "min. rozmiar (MiB)", + "s_s2": "maks. rozmiar (MiB)", + "s_d1": "min. data iso8601", + "s_d2": "maks. data iso8601", + "s_u1": "przesłane po", + "s_u2": "i/lub przed", + "s_r1": "ścieżka zawiera   (oddzielone spacją)", + "s_f1": "nazwa zawiera   (odwróć za pomocą -nope)", + "s_t1": "znaczniki zawierają   (^=start, koniec=$)", + "s_a1": "dokładne właściwości metadanych", + + "md_eshow": "nie można wyświetlić ", + "md_off": "[📜readme] wyłączone w [⚙️] -- dokument ukryty", + + "badreply": "Nie udało się przeanalizować odpowiedzi serwera", + + "xhr403": "403: Odmowa dostępu.\n\nSpróbuj przeładować stronę (F5), możliwe, że cię wylogowano", + "xhr0": "nieznany (być może utracono połączenie z serwerem, lub jest on nieaktywny)", + "cf_ok": "przepraszamy, włączyła się ochrona przed DD" + wah + "oS\n\nwszystko powinno wrócić do normy za około 30 sekund\n\njeśli nic się nie zmieni, naciśnij klawisz F5, aby przeładować stronę", + "tl_xe1": "nie można wyświetlić podfolderów:\n\nbłąd ", + "tl_xe2": "404: Nie znaleziono folderu", + "fl_xe1": "nie można wyświetlić plików w folderze:\n\nbłąd ", + "fl_xe2": "404: Nie znaleziono folderu", + "fd_xe1": "nie można stworzyć podfolderu:\n\nbłąd ", + "fd_xe2": "404: Nie znaleziono folderu nadrzędnego", + "fsm_xe1": "nie można wysłać wiadomości:\n\nbłąd ", + "fsm_xe2": "404: Nie znaleziono folderu nadrzędnego", + "fu_xe1": "nie udało się wczytać listy unpost z serwera:\n\nbłąd ", + "fu_xe2": "404: Nie znaleziono pliku??", + + "fz_tar": "nieskompresowane archiwum gnu-tar (linux / mac)", + "fz_pax": "nieskompresowane archiwum tar w formacie pax (wolniejsze)", + "fz_targz": "gnu-tar z kompresją gzip poziomu 3.,$N$Nzazwyczaj bardzo wolne, używaj nieskompresowanego tar", + "fz_tarxz": "gnu-tar z kompresją xz poziomu 3.$N$Nzazwyczaj bardzo wolne, używaj nieksompresowanego tar", + "fz_zip8": "zip z nazwami plików UTF-8 (może działać nieprawidłowo na systemie Windows 7 i starszych)", + "fz_zipd": "zip z nazwami plików cp437, dobre dla bardzo starego oprogramowania", + "fz_zipc": "cp437 z CRC32 obliczonym wcześniej,$Ndla MS-DOS PKZIP v2.04g (październik 1993)$N(przetwarzanie do pobrania trwa dłużej)", + + "un_m1": "można usunąć ostatnio przesłane pliki (lub przerwać trwające) poniżej", + "un_upd": "odśwież", + "un_m4": "lub udostępnij pliki widoczne poniżej:", + "un_ulist": "pokaż", + "un_ucopy": "kopiuj", + "un_flt": "filtruj (opcjonalnie):  URL musi zawierać", + "un_fclr": "wyczyść kryteria filtrowania", + "un_derr": 'nie udało się usunąć unpost:\n', + "un_f5": 'coś poszło nie tak, spróbuj odświeżyć lub wciśnij F5', + "un_uf5": "przed przerwaniem przesyłania trzeba odświeżyć stronę (za pomocą CTRL-R lub F5)", + "un_nou": 'ostrzeżenie: serwer jest aktualnie zbyt obciążony, żeby pokazać niedokończone przesłania; kliknij link "odśwież" za chwilę', + "un_noc": 'ostrzeżenie: unpost w pełni przesłanych plików jest wyłączone/zabronione w konfiguracji serwera', + "un_max": "pokazuję pierwsze 2000 plików (użyj filtrowania)", + "un_avail": "{0} ostatnio przesłanych elementów może zostać usunięte
{1} niedokończonych można przerwać", + "un_m2": "przesortowano po czasie przesłania; najnowsze elementy pierwsze: ", + "un_no1": "cholibka! żaden przesłany element nie jest wystarczająco niedawny", + "un_no2": "cholibka! żaden przesłany element pasujący do filtra nie jest wystarczająco niedawny", + "un_next": "usuń następne {0} pliki(ów) poniżej", + "un_abrt": "przerwij", + "un_del": "usuń", + "un_m3": "wczytywanie ostatnio przesłanych elementów...", + "un_busy": "usuwanie {0} plików...", + "un_clip": "skopiowano {0} linków do schowka", + + "u_https1": "powinieneś przejść", + "u_https2": "na HTTPS w celu", + "u_https3": "uzyskania lepszej wydajności", + "u_ancient": 'twoja przeglądarka jest niezwykle zabytkowa -- powinieneś zamiast tego użyć bup', + "u_nowork": "wymaga Firefox 53+, Chrome 57+ lub iOS 11+", + "tail_2old": "wymaga Firefox 105+, Chrome 71+ lub iOS 14.5+", + "u_nodrop": 'ta przeglądarka jest za stara, nie wspiera przesyłania "przeciągnij i upuść"', + "u_notdir": "to nie jest folder!\n\nta przeglądarka jest za stara\nspróbuj przeciągnąć i upuścić", + "u_uri": "aby przeciągnąć i upuścić obrazy z innych okien przeglądarki,\nupuść je na duży przycisk przesyłania", + "u_enpot": 'przełącz na lekki interfejs (może zwiększyć prędkość przesyłania)', + "u_depot": 'przełącz na ładny interfejs (może zmniejszyć prędkośc przesyłania)', + "u_gotpot": 'przełączanie na lekki interfejs w celu poprawy prędkości przesyłania,\n\nzawsze można przełączyć się na ładny interfejs!', + "u_pott": "

pliki:   {0} ukończonych,   {1} nie powiodło się,   {2} w trakcie,   {3} oczekujących

", + "u_ever": "podstawowe przesyłanie; up2k wymaga minimalnie przeglądarek:
Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1", + "u_su2k": 'podstawowe przesyłanie; up2k jest lepszy', + "u_uput": 'optymalizuj dla prędkości (pomijając spr. sum kontrolnych)', + "u_ewrite": 'nie masz dostępu do zapisu (write) w tym folderze', + "u_eread": 'nie masz dostępu do odczytu (read) tego folderu', + "u_enoi": 'wyszukiwanie plików jest wyłączone w konfiguracji serwera', + "u_enoow": "nadpisanie nie zadziała, wymagany dostęp do usuwania (delete)", + "u_badf": '{0} (z {1}) plików zostało pominiętych, prawdopodobnie przez opcje dostępu systemu plików:\n\n', + "u_blankf": '{0} (z {1}) plików jest pustych; przesłać mimo to?\n\n', + "u_applef": '{0} (z {1}) plików może być niepożądane;\nNaciśnij OK/Enter, aby pominąć je (wypisane poniżej);\nNaciśnij Anuluj/ESC, by je przesłać mimo to:\n\n', + "u_just1": '\nTa funkcja może działać lepiej z wybranym jednym plikiem', + "u_ff_many": "na systemach Linux / MacOS / Android, ta ilośc plików może spowodować przymusowe zamknięcie przeglądarki Firefox\nw takim przypadku, spróbuj ponownie (lub użyj Chrome'a).", + "u_up_life": "Ten przesyłany plik zostanie usunięty z serwera\n{0} po zakończeniu przesyłania", + "u_asku": 'prześlij {0} pliki(ów) do {1}', + "u_unpt": "można cofnąć / usunąć ten przesłany plik za pomocą 🧯 w lewym górnym rogu", + "u_bigtab": 'zaraz pokażę {0} plików\n\nta operacja może zawiesić twoją przeglądarkę, na pewno kontynuować?', + "u_scan": 'Skanowanie plików...', + "u_dirstuck": 'iterator katalogów utknął podczas próby dostępu poniższych {0} elementów, pominięto:', + "u_etadone": 'Ukończono ({0}, {1} plików)', + "u_etaprep": '(przygotowywanie do przesłania)', + "u_hashdone": 'obliczono sumę kontrolną', + "u_hashing": 'obliczanie sumy kontrolnej', + "u_hs": 'nawiązywanie połączenia...', + "u_started": "rozpoczęto przesyłanie; zobacz w [🚀]", + "u_dupdefer": "duplikat; zostanie przetworzony na końcu", + "u_actx": "kliknij ten napis, aby zapobiec spadkowi
wydajności po zmianie aktywnego okna/karty przeglądarki", + "u_fixed": "OK!  Naprawiono 👍", + "u_cuerr": "nie udało się przesłać fragmentu {0} z {1};\nprawdopodobnie niegroźne, kontynuowanie\n\nplik: {2}", + "u_cuerr2": "serwer odrzucił przesyłanie (kawałek {0} z {1});\nspróbuję ponownie później\n\nplik: {2}\n\nbłąd ", + "u_ehstmp": "spróbuję ponownie; więcej informacji w prawym dolnym rogu", + "u_ehsfin": "serwer odrzucił prośbę o zakończenie przesyłania; próbuję ponownie...", + "u_ehssrch": "serwer odrzucił prośbę o wykonanie wyszukania; próbuję ponownie...", + "u_ehsinit": "serwer odrzucił prośbę o rozpoczęcie przesyłania; próbuję ponownie...", + "u_eneths": "błąd sieci podczas negocjacji warunków przesyłania; próbuję ponownie...", + "u_enethd": "błąd sieci podczas sprawdzania istnienia celu; próbuję ponownie...", + "u_cbusy": "oczekiwanie na ponowne zaufanie serwera po błędzie sieci...", + "u_ehsdf": "brak miejsca na dysku serwera!\n\npróby będą ponawiane na wypadek\nzwolnienia wystarczająco dużo miejsca aby kontynuować", + "u_emtleak1": "wygląda na to, że twoja przeglądarka może mieć wyciek pamięci;\n", + "u_emtleak2": ' przejdź na HTTPS (zalecane) lub ', + "u_emtleak3": ' ', + "u_emtleakc": 'spróbuj:\n
  • wciśnij F5, aby odświeżyć stronę
  • wyłącz przycisk  ww  w menu ⚙️ ustawienia
  • i spróbuj przesłać ponownie
Prędkość przesyłania będzie niższa, ale cóż zrobisz.\nPrzepraszamy za problemu!\n\nPS: Chrome v107 ma poprawkę tego błędu.', + "u_emtleakf": 'spróbuj:\n
  • wciśnij F5, aby odświeżyć stronę
  • włącz tryb 🥔 (lekkiego interfejsu) w interfejsie przesyłania
  • i spróbuj przesłać ponownie
\nPS: Firefox może kiedyś mieć poprawkę tego błędu', + "u_s404": "nie znaleziono na serwerze", + "u_expl": "wytłumacz", + "u_maxconn": "większość przeglądarek ogranicza to do 6, ale Firefox pozwala zwiększyć tą wartość, ustawiając connections-per-server w about:config", + "u_tu": '

UWAGA: tryb turbo włączony,  klient może nie wykryć i nie kontynuować niedokończonych przesłań; patrz wskazówka przycisku turbo

', + "u_ts": '

UWAGA: tryb turbo włączony,  wyniki wyszukiwania mogą być niepoprawne; patrz wskazówka przycisku turbo

', + "u_turbo_c": "tryb turbo jest wyłączony w konfiguracji serwera", + "u_turbo_g": "wyłączanie trybu turbo, nie posiadasz dostępu\ndo listy katalogu w tym wolumenie", + "u_life_cfg": 'autousuwanie po min (lub godz.)', + "u_life_est": 'przesłany plik zostanie usunięty ---', + "u_life_max": 'ten folder wymaga\nmaks. czasu do usunięcia równego {0}', + "u_unp_ok": 'unpost jest dozwolony przez {0}', + "u_unp_ng": 'unpost NIE jest dozwolony', + "ue_ro": 'dostęp tylko-do-odczytu\n\n', + "ue_nl": 'nie jesteś zalogowany', + "ue_la": 'zalogowano jako "{0}"', + "ue_sr": 'jesteś w trybie wyszukiwania\n\nprzełącz się na tryb przesyłania, klikając lupę 🔎 (obok przycisku Szukaj), i spróbuj ponownie', + "ue_ta": 'spróbuj przesłać ponownie, wszystko powinno być w porządku', + "ue_ab": "ten plik już jest przesyłany do innego folderu, przesyłanie musi się zakończyć, zanim będzie mógł być on przesłany gdzie indziej.\n\nMożna przerwać pierwsze przesyłanie za pomocą 🧯 w lewym górnym rogu", + "ur_1uo": "OK: Plik przesłany pomyślnie", + "ur_auo": "OK: Wszystkie ({0}) pliki zostały przesłane pomyślnie", + "ur_1so": "OK: Znaleziono plik na serwerze", + "ur_aso": "OK: Znaleziono wszystkie ({0}) pliki na serwerze", + "ur_1un": "Przesyłanie nie powiodło się", + "ur_aun": "Wszystkie ({0}) przesłania nie powiodły się", + "ur_1sn": "NIE znaleziono pliku na serwerze", + "ur_asn": "NIE znaleziono {0} plików na serwerze", + "ur_um": "Zakończono;\n{0} przesłań OK,\n{1} przesłań nie powiodło się", + "ur_sm": "Zakończono;\nznaleziono {0} pliki(ów),\nnie znaleziono {1} pliki(ów) na serwerze", + + "lang_set": "odśwież stronę (F5), aby zastosować zmianę.", + }, "rus": { "tt": "Русский", diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 6ffd95f6..2475cb97 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -385,6 +385,48 @@ var Ls = { "af1": "vis nylig opplasta filer", "ag1": "vis kjente IdP-brukarar", }, + "pol": { + "a1": "odśwież", + "b1": "witaj, nieznajomy   (nie jesteś zalogowany)", + "c1": "wyloguj się", + "d1": "zrzut stosu", + "d2": "pokazuje status wszystkich aktywnych wątków", + "e1": "przeładuj konfigurację", + "e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać", + "f1": "możesz przeglądać:", + "g1": "możesz przesyłać do:", + "cc1": "inne:", + "h1": "wyłącz k304", + "i1": "włącz k304", + "j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, ale spowolni ogólne działanie", + "k1": "zresetuj ustawienia klienta", + "l1": "zaloguj się po więcej:", + "m1": "Witaj,", + "n1": "404 nie znaleziono  ┐( ´ -`)┌", + "o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub przejdź do strony głównej', + "p1": "403 odmowa dostępu  ~┻━┻", + "q1": 'użyj hasła lub przejdź do strony głównej', + "r1": "idź do strony głównej", + ".s1": "przeskanuj ponownie", + "t1": "akcje", + "u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund", + "v1": "połącz", + "v2": "używaj tego serwera jako dysku lokalnego", + "w1": "przejdź na HTTPS", + "x1": "zmień hasło", + "y1": "edytuj udostępnione", + "z1": "odblokuj udostępnienie:", + "ta1": "najpierw wprowadź nowe hasło", + "ta2": "powtórz hasło dla potwierdzenia:", + "ta3": "znaleziono literówkę, spróbuj ponownie", + "aa1": "pliki przychodzące:", + "ab1": "wyłącz no304", + "ac1": "włącz no304", + "ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!", + "ae1": "trwające pobierania:", + "af1": "pokaż ostatnio przesłane pliki", + "ag1": "pokaż znanych użytkowników IdP", + }, "spa": { "a1": "actualizar", "b1": "hola   (no has iniciado sesión)", From 91ce7a29aa3324a8e321d002e62fbf4e5b1f13ec Mon Sep 17 00:00:00 2001 From: Chloe Surett Date: Sat, 9 Aug 2025 16:17:20 -0400 Subject: [PATCH 003/154] Add .idea to .gitignore (#547) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b7b533b4..b70c56b7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ scripts/docker/*.err # nix build output link result + +# IDEA config +.idea/ From e9ddfccfb60f43a66e04f463a2084c666d4cb557 Mon Sep 17 00:00:00 2001 From: Tr3yWay996 <112483771+Tr3yWay996@users.noreply.github.com> Date: Sat, 9 Aug 2025 22:26:52 +0200 Subject: [PATCH 004/154] Add French translation (#486) Add French translation (#486) --------- Signed-off-by: ed Co-authored-by: Packingdustry Co-authored-by: Andrew Lee Co-authored-by: A. Jakubiak --- copyparty/web/browser.js | 631 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 630 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index b3d174f8..f6b1e3eb 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -3785,6 +3785,635 @@ var Ls = { "lang_set": "ladataanko sivu uudestaan kielen vaihtamiseksi?", }, + "fra": { + "tt": "français", + + "cols": { + "c": "bouton d'action", + "dur": "durée", + "q": "qualité / débit binaire", + "Ac": "codec audio", + "Vc": "codec vidéo", + "Fmt": "format / conteneur", + "Ahash": "somme de contrôle audio", + "Vhash": "somme de contrôle vidéo", + "Res": "résolution", + "T": "type de fichier", + "aq": "qualité audio / débit binaire", + "vq": "qualité vidéo / débit binaire", + "pixfmt": "sous-échantillonnage / structure de pixel", + "resw": "résolution horizontale", + "resh": "résolution verticale", + "chs": "canaux audio", + "hz": "fréquence" + }, + + "hks": [ + [ + "misc", + ["Échap", "ferme divers menus"], + + "gestionaire de fichiers", + ["G", "activer vue en liste / vue en grille"], + ["T", "activer les miniatures / icônes"], + ["⇧ A/D", "taille des miniatures"], + ["ctrl-K", "suprimer la sélection"], + ["ctrl-X", "couper la sélection au presse-papier"], + ["ctrl-C", "copier la sélection au presse-papier"], + ["ctrl-V", "coller (déplacer/copier) ici"], + ["Y", "télécharger la sélection"], + ["F2", "renomer la sélection"], + + "file-list-sel", + ["Espace", "activer la sélection de fichiers"], + ["↑/↓", "déplacer le selecteur"], + ["ctrl ↑/↓", "déplacer le curseur et la zone d'affichage"], + ["⇧ ↑/↓", "sélectioner le fichier précédent/suivant"], + ["ctrl-A", "sélectionner tout les fichiers / dossiers"], + ], [ + "navigation", + ["B", "basculer la vue en fil d'Ariane / panneau de navigation"], + ["I/K", "dossier précédent/suivant"], + ["M", "dossier parent (ou réduire le dossier actuel)"], + ["V", "activer les dossiers / fichiers texte dans le volet de navigation"], + ["A/D", "taille du volet de navigation"], + ], [ + "lecteur-audio", + ["J/L", "chanson précédente/suivante"], + ["U/O", "sauter 10s en arrière/avant"], + ["0..9", "sauter à 0%..90%"], + ["P", "lecture/pause (démarre également la lecture)"], + ["S", "sélectionner la chanson en cours"], + ["Y", "télécharger le morceau"], + ], [ + "visionneuse d'image", + ["J/L, ←/→", "image précédente/suivante"], + ["Début/Fin, ⭦/Fin", "première/dernière image"], + ["F", "plein écran"], + ["R", "rotation horaire"], + ["⇧ R", "rotation antihoraire"], + ["S", "sélectionner l'image"], + ["Y", "télécharger l'image"], + ], [ + "lecteur vidéo", + ["U/O", "sauter 10s en arrière/avant"], + ["P/K/Espace", "lecture/pause"], + ["C", "continuer de lire la suivante"], + ["V", "lire en boucle"], + ["M", "couper le son"], + ["[ and ]", "définir l'intervalle de boucle"], + ], [ + "visionneuse de texte", + ["I/K", "fichier précédent/suivant"], + ["M", "fermer le fichier texte"], + ["E", "modifier le fichier texte"], + ["S", "sélectioner le fichier (pour le couper/copier/renommer)"], + ] + ], + + "m_ok": "OK", + "m_ng": "Annuler", + + "enable": "Activer", + "danger": "DANGER", + "clipped": "copié dans le presse-papier", + + "ht_s1": "seconde", + "ht_s2": "secondes", + "ht_m1": "minute", + "ht_m2": "minutes", + "ht_h1": "heure", + "ht_h2": "heures", + "ht_d1": "jour", + "ht_d2": "jours", + "ht_and": " et ", + + "goh": "panneau-de-commande", + "gop": 'élément "frère" précédent">précédent', + "gou": 'dossier parent">haut', + "gon": 'dossier suivant">suivant', + "logout": "Déconnexion ", + "access": "accès", + "ot_close": "fermer le sous-menu", + "ot_search": "chercher des fichiers par leurs attributs, chemin / nom, tag musicaux, ou nimporte quelle combinaison de ces options$N$N<code>foo bar</code> = doit contenir à la fois «foo» et «bar»,$N<code>foo -bar</code> = doit contenir «foo» mais pas «bar»,$N<code>^yana .opus$</code> = commence par «yana» et est un fichier «opus»$N<code>"try unite"</code> = contient exactement «try unite»$N$Nle format de date est iso-8601, comme$N<code>2009-12-31</code> ou <code>2020-09-12 23:30:00</code>", + "ot_unpost": "unpost: supprimer vos téléchargements récents, ou annuler ceux en cours", + "ot_bup": "bup: téléverseur de base, prend même en charge netscape 4.0", + "ot_mkdir": "mkdir: créer un nouveau répertoire", + "ot_md": "new-md: créer un nouveau document markdown", + "ot_msg": "msg: envoyer un message au journal du serveur", + "ot_mp": "options du lecteur multimedia", + "ot_cfg": "options de configuration", + "ot_u2i": 'up2k : téléverser des fichiers (si vous avez un accès en écriture) ou basculer en mode recherche pour voir s\'ils existent quelque part sur le serveur$N$Nles téléversements peuvent être repris, ils sont multithreadé, et les horodatages des fichiers sont préservés, mais cela utilise plus de CPU que [🎈]  (le téléverseur de base)

pendant les téléversements, cette icône devient un indicateur de progression!', + "ot_u2w": 'up2k : téléverser des fichiers avec prise en charge de la reprise (fermez votre navigateur et déposez les mêmes fichiers plus tard)$N$multithreadé, et les horodatages des fichiers sont préservés, mais cela utilise plus de CPU que [🎈]  (le téléverseur de base)

pendant les téléversements, cette icône devient un indicateur de progression!', + "ot_noie": 'Utilisez Chrome / Firefox / Edge', + + "ab_mkdir": "créer un nouveau répertoire", + "ab_mkdoc": "faire un nouveau document markdown", + "ab_msg": "envoyer un message au journal du serveur", + + "ay_path": "passer aux dossiers", + "ay_files": "passer aux fichiers", + + "wt_ren": "renommer les éléments sélectionnés$NHotkey: F2", + "wt_del": "supprimer les éléments sélectionnés$NHotkey: ctrl-K", + "wt_cut": "couper les éléments sélectionnés <small>(puis coller ailleurs)</small>$NHotkey: ctrl-X", + "wt_cpy": "copier les éléments sélectionnés dans le presse-papiers$N(pour les coller ailleurs)$NHotkey: ctrl-C", + "wt_pst": "coller une sélection précédemment coupée / copiée$NHotkey: ctrl-V", + "wt_selall": "sélectionner tous les fichiers$NHotkey: ctrl-A (lorsque le fichier est sélectionné)", + "wt_selinv": "inverser la sélection", + "wt_zip1": "télécharger ce dossier en tant qu'archive", + "wt_selzip": "télécharger la sélection en tant qu'archive", + "wt_seldl": "télécharger la sélection en tant que fichiers séparés$NHotkey: Y", + "wt_npirc": "copier les informations de la musique au format irc", + "wt_nptxt": "copier les informations de la musique en texte brut", + "wt_m3ua": "ajouter à la playlist m3u (cliquez sur 📻copier plus tard)", + "wt_m3uc": "copier la playlist m3u dans le presse-papiers", + "wt_grid": "basculer entre la vue en grille / liste$NHotkey: G", + "wt_prev": "musique précédente$NHotkey: J", + "wt_play": "lecture / pause$NHotkey: P", + "wt_next": "musique suivante$NHotkey: L", + + "ul_par": "téléversements parallèles:", + "ut_rand": "attribution de noms de fichiers aléatoires", + "ut_u2ts": "copier l'horodatage de dernière modification$Nde votre système de fichiers vers le serveur\">📅", + "ut_ow": "écraser les fichiers existants sur le serveur?$N🛡️: jamais (générera un nouveau nom de fichier à la place)$N🕒: écraser si le fichier sur le serveur est plus ancien que le vôtre$N♻️: toujours écraser si les fichiers sont différents", + "ut_mt": "continuer à calculer la somme de contrôle d'autres fichiers pendant le téléversement$N$Npeut-être désactiver si votre CPU ou HDD est la cause de perte de performances", + "ut_ask": 'demander confirmation avant le début du téléversement">💭', + "ut_pot": "améliorer la vitesse de téléversement sur les appareils lents$Nen simplifiant l'interface utilisateur", + "ut_srch": "ne pas réellement téléverser, mais vérifier si les fichiers existent déjà$N sur le serveur (scannera tous les dossiers que vous pouvez lire)", + "ut_par": "mettre en pause les téléversements en le réglant sur 0$N$Naugmenter si votre connexion est lente / à forte latence$N$Nle garder à 1 sur le LAN ou si le HDD du serveur est un goulot d'étranglement", + "ul_btn": "déposer des fichiers / dossiers
ici (ou cliquez sur moi)", + "ul_btnu": "T É L É V E R S E R", + "ul_btns": "C H E R C H E R", + + "ul_hash": "somme de contrôle", + "ul_send": "envoyer", + "ul_done": "terminé", + "ul_idle1": "aucun téléversement n'est encore dans la file d'attente", + "ut_etah": "moyenne <em>hashing</em> vitesse, et temps estimé jusqu'à la fin", + "ut_etau": "moyenne <em>upload</em> vitesse et temps estimé jusqu'à la fin", + "ut_etat": "moyenne <em>total</em> vitesse et temps estimé jusqu'à la fin", + + "uct_ok": "terminé avec succès", + "uct_ng": "non réussi : échoué / rejeté / non trouvé", + "uct_done": "terminés et échoué combinés", + "uct_bz": "hachage ou téléversement", + "uct_q": "inactif, en attente", + + "utl_name": "nom de fichier", + "utl_ulist": "liste", + "utl_ucopy": "copie", + "utl_links": "liens", + "utl_stat": "état", + "utl_prog": "progrès", + + // keep short: + "utl_404": "404", + "utl_err": "ERREUR", + "utl_oserr": "OS-ERREUR", + "utl_found": "trouvé", + "utl_defer": "état", + "utl_yolo": "YOLO", + "utl_done": "terminé", + + "ul_flagblk": "les fichiers ont été ajoutés à la file d'attente
cependant, il y a un processus up2k actif dans un autre onglet du navigateur,
en attente qu'il finisse d'abord", + "ul_btnlk": "la configuration du serveur a verrouillé cette options dans cet état", + + "udt_up": "Téléverser", + "udt_srch": "Chercher", + "udt_drop": "déposer ici", + + "u_nav_m": '
aight, ques-que tu à ?
Enter = Fichiers (un ou plus)\nESC = Un dossier (sous-dossiers inclus)', + "u_nav_b": 'FichiersUn dossier', + + "cl_opts": "options", + "cl_themes": "thème", + "cl_langs": "langue", + "cl_ziptype": "téléchargement de dossier", + "cl_uopts": "up2k", + "cl_favico": "favicon", + "cl_bigdir": "gros dossiers", + "cl_hsort": "#sort", + "cl_keytype": "notation des touches", + "cl_hiddenc": "colonnes masquées", + "cl_hidec": "masquer", + "cl_reset": "réinitialiser", + "cl_hpick": "cliquez sur les en-têtes de colonnes pour les masquer dans le tableau ci-dessous", + "cl_hcancel": "masquage des colonnes annulé", + + "ct_grid": '田 grille', + "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_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', + "ct_qdel": 'ne demander qu\'une confirmation lors de la suppression de fichiers>qdel', + "ct_dir1st": 'trier les dossiers avant les fichiers">📁 first', + "ct_nsort": 'triage par numérotation (pour les nom de fichiers qui sont numérotés)">nsort', + "ct_utc": 'voir tout les horodatage en format UTC">UTC', + "ct_readme": 'voir le fichier README.md dans le listage des dossiers">📜 readme', + "ct_idxh": 'voir une version html (index.html) au-lieu du listage des dossiers normal">htm', + "ct_sbars": 'montrer la barre de defilement">⟊', + + "cut_umod": "si un fichier existe déjà sur le server, mettre à jour l'horodatage de dernière modification du serveur pour qu'il corresponde à votre fichier local (nécessite des autorisations d'écriture et de suppression)\">re📅", + + "cut_turbo": "le bouton yolo, vous ne voulez probablement PAS activer ceci:$N$Nutilisez ceci si vous téléchargez une grande quantité de fichiers et que vous devez redémarrer pour une raison quelconque, et que vous souhaitez continuer le téléchargement dès que possible$N$Ncela remplace la vérification de hachage par une simple "est-ce que cela a la même taille de fichier sur le serveur?" donc si le contenu du fichier est différent, il ne sera PAS téléchargé$N$Nvous devriez désactiver cela lorsque le téléchargement est terminé, puis "télécharger" les mêmes fichiers à nouveau pour laisser le client les vérifier\">turbo", + + "cut_datechk": "n'a aucun effet à moins que le bouton turbo ne soit activé$N$Nréduit le facteur yolo d'un tout petit peu ; vérifie si les horodatages des fichiers sur le serveur correspondent aux vôtres$N$Ndevrait théoriquement attraper la plupart des téléchargements inachevés / corrompus, mais n'est pas un substitut à un passage de vérification avec turbo désactivé par la suite\">date-chk", + + "cut_u2sz": "taille (en MiB) de chaque morceau de téléversement; des grosse valeurs vont mieux passer si la distance entre le serveur et vous est trés grande. Si vous avez une connection trés instable, essayer de plus petites valeurs", + + "cut_flag": "s'assurer qu'un seul onglet est entrain de mettre un fichier en ligne a la fois $N -- les autres onglets doivent avoir cette option activé aussi $N -- affecte seulement les onglets qui sont sur le même domaine", + + "cut_az": "mettre en ligne les fichiers dans l'ordre alphabétique, plutôt que le plus petit fichier en premier$N$Nl'ordre alphabétique peut rendre la lecture plus douce sur pour les yeux si quelque chose s'est mal passé sur le serveur, mais cela rend le téléversement légèrement plus lent sur fibre / LAN", + + "cut_nag": "recevoir une notification via l'OS quand un téléversement finit$N(seulement si le navigateur ou l'onglet n'est pas actif)", + "cut_sfx": "alerte audible quand le téléversement finit$N(seulement si le navigateur ou l'onglet n'est pas actif)", + + "cut_mt": "utiliser le calcul de somme de contrôle multithreadé pour accelerer le processus$N$Ncela utilise des web-workers et nécessite$Nplus de RAM (jusqu'à 512 MiB supplémentaires)$N$NCela rend https 30% plus rapide, http 4.5x plus rapide\">mt", + + "cut_wasm": "utiliser wasm au lieu du hachage intégré du navigateur; améliore la vitesse sur les navigateurs basés sur chrome mais augmente la charge CPU, et de nombreuses anciennes versions de chrome ont des bugs qui font que le navigateur consomme toute la RAM et plante si cela est activé\">wasm", + + "cft_text": "text favicon (laisser vide et rafraîchir pour désactiver)", + "cft_fg": "couleur de premier plan", + "cft_bg": "couleur d'arrière-plan", + + "cdt_lim": "nombre maximum de fichiers à afficher dans un dossier", + "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.", + + "tt_entree": "afficher le panneau de navigation (arborescence des dossiers)$NHotkey: B", + "tt_detree": "afficher le fil d’Ariane$NHotkey: B", + "tt_visdir": "faire défiler jusqu'au dossier sélectionné", + "tt_ftree": "basculer l'arborescence des dossiers / fichiers texte$NHotkey: V", + "tt_pdock": "afficher les dossiers parents dans un panneau ancré en haut", + "tt_dynt": "croissance automatique à mesure que l'arborescence s'étend", + "tt_wrap": "retour à la ligne", + "tt_hover": "révéler les lignes débordantes au survol$N( interrompt le défilement à moins que le curseur de la souris ne soit dans la gouttière gauche )", + + "ml_pmode": "à la fin du dossier…", + "ml_btns": "cmds", + "ml_tcode": "transcoder", + "ml_tcode2": "transcoder vers", + "ml_tint": "teinte", + "ml_eq": "égaliseur audio", + "ml_drc": "compresseur de plage dynamique", + + "mt_loop": "répéter en boucle une musique\">🔁", + "mt_one": "stopper après une musique\">1️⃣", + "mt_shuf": "mélanger les musiques dans chaque dossiers\">🔀", + "mt_aplay": "jouer automatiquement si le lien utilisé pour accéder au serveur a un song-ID $N$N, désactiver cela arrêtera également la mise à jour de l'URL de la page avec les song-IDs lors de la lecture de la musique, pour éviter la lecture automatique si ces paramètres sont perdus mais que l'URL reste\">a▶", + "mt_preload": "commencer à charger la prochaine chanson près de la fin pour une lecture sans interruption\">preload", + "mt_prescan": "explorer le dossier suivant avant la dernière musique$Nne finisse, pour garder le navigateur content$Npour qu'il n'arrête pas la lecture\">nav", + "mt_fullpre": "essayer de pré-charger la musique entière;$N✅ activer en cas de connection instable,$N❌ désactiver en revanche sur une connection lente va probablement être mieux\">full", + "mt_fau": "sur téléphone, empêche la musique de s'arrêter de jouer si la prochaine n'est pas pré-chargée assez rapidement (peut rendre l'affichage des tags buggé)\">☕️", + "mt_waves": "barre de progression en spectrograme:$Nmontrer l'amplitude audio dans la miniature\">~s", + "mt_npclip": "montrer les boutons pour copier le morceau en cours de lecture\">/np", + "mt_m3u_c": "montrer les boutons pour copier les$morceaux sélectionnées en tant qu'entrées de playlist m3u8\">📻", + "mt_octl": "intégration os (touches de raccourci multimédia / osd)\">os-ctl", + "mt_oseek": "permettre la recherche via l'intégration os$N$Nremarque : sur certains appareils (iPhones),$Ncela remplace le bouton de la chanson suivante\">seek", + "mt_oscv": "montrer la couverture de l'album dans l'osd\">art", + "mt_follow": "garder la piste en cours défilée dans la vue\">🎯", + "mt_compact": "contrôles compacts\">⟎", + "mt_uncache": "effacer le cache  (essayez ceci si votre navigateur a mis en cache$Nun copie défectueuse d'une chanson, ce qui empêche sa lecture)\">uncache", + "mt_mloop": "lire en boucle le dossier ouvert\">🔁 loop", + "mt_mnext": "charger le dossier suivant et continuer\">📂 next", + "mt_mstop": "arrêter la lecture\">⏸ stop", + "mt_cflac": "convertir flac / wav en opus\">flac", + "mt_caac": "convertir aac / m4a en opus\">aac", + "mt_coth": "convertir tout les autres (pas mp3) en opus\">oth", + "mt_c2opus": "meilleur choix pour PC fixe, PC portable, android\">opus", + "mt_c2owa": "opus-weba, pour iOS 17.5 et supérieur\">owa", + "mt_c2caf": "opus-caf, pour iOS 11 à 17\">caf", + "mt_c2mp3": "utilisez ceci sur des appareils très anciens\">mp3", + "mt_c2flac": "meilleure qualité sonore, mais téléchargements énormes\">flac", + "mt_c2wav": "lecture non compressée (encore plus gros)\">wav", + "mt_c2ok": "bien, bon choix", + "mt_c2nd": "ce n'est pas le format de sortie recommandé pour votre appareil, mais ça devrait aller", + "mt_c2ng": "votre appareil ne semble pas prendre en charge ce format de sortie, mais essayons quand même", + "mt_xowa": "il y a des bugs dans iOS qui empeche d'avoir une lecture en ariere plan en utilisant ce format; utilisez caf ou mp3 à la place", + "mt_tint": "niveau d’arrière-plan (0–100) de la barre de progression$Npour rendre la mise en mémoire tampon moins gênante", + "mt_eq": "active l'égaliseur et le contrôle de gain;$N$Nboost <code>0</code> = volume standard 100% (non modifié)$N$Nwidth <code>1  </code> = stéréo standard (non modifié)$Nwidth <code>0.5</code> = 50% de crossfeed gauche-droite$Nwidth <code>0  </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = suppression vocale :^)$N$Nl'activation de l'égaliseur rend les albums gapless entièrement gapless, alors laissez-le activé avec toutes les valeurs à zéro (sauf largeur = 1) si vous vous en souciez", + "mt_drc": "active le compresseur de plage dynamique (aplanisseur de volume / brickwaller); activera également l'EQ pour équilibrer les choses, donc définissez tous les champs EQ sauf 'width' sur 0 si vous ne le voulez pas$N$Ndiminue le volume de l'audio au-dessus de THRESHOLD dB; pour chaque RATIO dB au-delà de THRESHOLD, il y a 1 dB de sortie, donc des valeurs par défaut de tresh -24 et ratio 12 signifient qu'il ne devrait jamais être plus fort que -22 dB et qu'il est sûr d'augmenter le boost de l'égaliseur à 0.8, ou même 1.8 avec ATK 0 et un énorme RLS comme 90 (ne fonctionne que dans firefox; RLS est max 1 dans les autres navigateurs)$N$N(voir wikipedia, ils expliquent cela beaucoup mieux)", + + "mb_play": "lecture", + "mm_hashplay": "lire ce fichier audio ?", + "mm_m3u": "appuyez sur Entrée/OK pour lire\nappuyez sur Échap/Annuler pour modifier", + "mp_breq": "nécessite firefox 82+ ou chrome 73+ ou iOS 15+", + "mm_bload": "chargement en cours…", + "mm_bconv": "conversion en {0}, veuillez patienter…", + "mm_opusen": "votre navigateur ne peut pas lire les fichiers aac / m4a ;\nle transcodage en opus est maintenant activé", + "mm_playerr": "échec de la lecture : ", + "mm_eabrt": "La tentative de lecture a été annulée", + "mm_enet": "Votre connexion internet est instable ou inexistante", + "mm_edec": "Ce fichier est supposément corrompu??", + "mm_esupp": "Votre navigateur ne comprend pas ce format audio", + "mm_eunk": "Erreur inconnue", + "mm_e404": "Impossible de lire l'audio ; erreur 404 : fichier introuvable.", + "mm_e403": "Impossible de lire l'audio ; erreur 403 : accès refusé.\n\nEssayez d'appuyer sur F5 pour recharger, peut-être que vous avez été déconnecté", + "mm_e500": "Impossible de lire l'audio ; erreur 500 : vérifiez les journaux du serveur.", + "mm_e5xx": "Impossible de lire l'audio ; erreur serveur ", + "mm_nof": "Pas d'autres fichiers audio trouvés par ici", + "mm_prescan": "En recherche d'une autre musique à lire…", + "mm_scank": "Prochaine musique trouvée :", + "mm_uncache": "cache vidé ; toutes les chansons seront retéléchargées lors de la prochaine lecture", + "mm_hnf": "cette chanson n'existe plus", + + "im_hnf": "cette image n'existe plus", + + "f_empty": 'ce dossier est vide', + "f_chide": 'ceci va cacher les colonnes «{0}»\n\ntu peut les réafficher dans les options', + "f_bigtxt": "ce fichier fait {0} MiB -- tu veut vraiment le voir en tant que texte ?", + "f_bigtxt2": "voir seulement la fin du fichier à la place ? ceci activera aussi le suivi en temps réel, affichant les nouvelles lignes de texte au fur et à mesure", + "fbd_more": '
showing {0} of {1} files; show {2} or show all
', + "fbd_all": '
showing {0} of {1} files; show all
', + "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_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 CANCEL 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.", + + "ft_paste": "coller {0} éléments$NHotkey: ctrl-V", + "fr_eperm": 'impossible de renommer:\n vous n\'avez pas la permission “move” dans ce dossier', + "fd_eperm": 'impossible de supprimer:\nvous n\'avez pas la permission “delete” dans ce dossier', + "fc_eperm": 'impossible de couper:\nvous n\'avez pas la permission “move” dans ce dossier', + "fp_eperm": 'impossible de coller:\nvous n\'avez pas la permission “write” dans ce dossier', + "fr_emore": "sélectionnez au moins un élément à renommer", + "fd_emore": "sélectionnez au moins un élément à supprimer", + "fc_emore": "sélectionnez au moins un élément à couper", + "fcp_emore": "sélectionnez au moins un élément à copier dans le presse-papiers", + + "fs_sc": "partager le dossier dans lequel vous vous trouvez", + "fs_ss": "partager les fichiers sélectionnés", + "fs_just1d": "vous ne pouvez pas sélectionner plus d'un dossier,\nou mélanger des fichiers et des dossiers dans une seule sélection", + "fs_abrt": "❌ abandonner", + "fs_rand": "🎲 nom.aleatoire", + "fs_go": "✅ créer partage", + "fs_name": "nom", + "fs_src": "source", + "fs_pwd": "mdp", + "fs_exp": "expiration", + "fs_tmin": "min", + "fs_thrs": "heures", + "fs_tdays": "jours", + "fs_never": "éternel", + "fs_pname": "nom de lien optionnel ; sera aléatoire si vide", + "fs_tsrc": "le fichier ou le dossier à partager", + "fs_ppwd": "mot de passe optionnel", + "fs_w8": "création du partage…", + "fs_ok": "appuyez sur Entrée/OK pour le Presse-papiers\nappuyez sur Échap/Annuler pour fermer", + + "frt_dec": "peut potentiellement réparer certaines instances de noms de fichiers cassés\">url-decode", + "frt_rst": "réinitialiser les noms de fichiers modifiés à leurs originaux\">↺ reset", + "frt_abrt": "abandonner et fermer cette fenêtre\">❌ cancel", + "frb_apply": "APPLIQUER RENOMMER", + "fr_adv": "renommage par lot / métadonnées / motif\">advanced", + "fr_case": "regex sensible à la casse\">case", + "fr_win": "noms windows-safe; remplacer <>:"\\|?* par des caractères japonais en pleine largeur\">win", + "fr_slash": "remplacer / par un caractère qui ne provoque pas la création de nouveaux dossiers\">no /", + "fr_re": "modèle de recherche regex à appliquer aux noms de fichiers originaux ; les groupes capturés peuvent être référencés dans le champ de format ci-dessous comme <code>(1)</code> et <code>(2)</code> et ainsi de suite", + "fr_fmt": "inspiré par foobar2000 : $N<code>(title)</code> est remplacé par le titre de la chanson, $N<code>[(artist) - ](title)</code> saute [cette] partie si l'artiste est vide, $N<code>$lpad((tn),2,0)</code> remplit le numéro de piste à 2 chiffres", + "fr_pdel": "supprimer", + "fr_pnew": "enregistrer sous", + "fr_pname": "donnez un nom pour le nouveau preset", + "fr_aborted": "abandonné", + "fr_lold": "ancien nom", + "fr_lnew": "nouveau nom", + "fr_tags": "tags pour les fichier selectioné (lecture-seule, juste pour référence):", + "fr_busy": "renomage de {0} items…\n\n{1}", + "fr_efail": "renomage a échoué:\n", + "fr_nchg": "{0} des nouveaux noms ont été modifiés en raison de win et/ou no /\n\nOK pour continuer avec ces nouveaux noms modifiés ?", + + "fd_ok": "suppression réussie", + "fd_err": "impossible de supprimer:\n", + "fd_none": "rien n'a été supprimé ; peut-être bloqué par la configuration du serveur (xbd) ?", + "fd_busy": "suppression de {0} éléments…\n\n{1}", + "fd_warn1": "SUPPRIMER ces {0} éléments ?", + "fd_warn2": "Dernière chance ! Impossible de revenir en arrière. Supprimer ?", + + "fc_ok": "couper {0} éléments", + "fc_warn": 'couper {0} éléments\n\nmais : seul cet onglets peut les coller\n(puisque la sélection est si absolument massive)', + + "fcc_ok": "copié {0} éléments dans le presse-papiers", + "fcc_warn": 'copié {0} éléments dans le presse-papiers\n\nmais : seul cet onglet peut les coller\n(puisque la sélection est si absolument massive)', + + "fp_apply": "utiliser ces noms", + "fp_ecut": "en premier, coupez ou copiez quelques fichiers / dossiers à coller / déplacer\n\nnote: vous pouvez couper / coller a travers different onglets", + "fp_ename": "{0} éléments ne peuvent pas être déplacés ici parceque leurs noms sont déjà pris. Donnez leurs un nouveau nom ci-dessous pour continuer, ou laissez les vides pour les sauter:", + "fcp_ename": "{0} éléments ne peuvent pas être copiés ici parce que les noms sont déjà pris. Donnez-leur un nouveau nom ci-dessous pour continuer, ou laissez-les vides pour les sauter :", + "fp_emore": "il reste encore des collisions de noms de fichiers à corriger", + "fp_ok": "déplacement OK", + "fcp_ok": "copie OK", + "fp_busy": "déplacement de {0} éléments…\n\n{1}", + "fcp_busy": "copie de {0} éléments…\n\n{1}", + "fp_err": "deplacement échoué:\n", + "fcp_err": "copie échouée:\n", + "fp_confirm": "déplacer ces {0} éléments ici ?", + "fcp_confirm": "copier ces {0} éléments ici ?", + "fp_etab": 'lecture du presse-papier venant d\'un autre onglet échoué', + "fp_name": "téléversement d'un fichier de votre apareil. Donnez lui un nom:", + "fp_both_m": '
choisisez ce qu\'il faut coller
Entrer = Déplacer {0} fichiers de «{1}»\nESC = Téléverser {2} fichiers de votre appareil', + "fcp_both_m": '
choisissez ce qu\'il faut coller
Entrer = Copier {0} fichiers de «{1}»\nESC = Téléverser {2} fichiers de votre appareil', + "fp_both_b": 'DéplacerTéléverser', + "fcp_both_b": 'CopierTéléverser', + + "mk_noname": "entrez un nom dans le champ de texte à gauche avant de faire ça :p", + + "tv_load": "Chargement du document texte:\n\n{0}\n\n{1}% ({2} de {3} MiB chargés)", + "tv_xe1": "impossible de charger le fichier texte:\n\nerreur", + "tv_xe2": "404, fichier introuvable", + "tv_lst": "liste des fichiers texte dans", + "tvt_close": "retour a la vue de dossier$NHotkey: M (ou Échap)\">❌ fermer", + "tvt_dl": "télécharger ce fichier$NHotkey: Y\">💾 télécharger", + "tvt_prev": "montrer le document précédent$NHotkey: i\">⬆ précédent", + "tvt_next": "montrer le document suivant$NHotkey: K\">⬇ suivant", + "tvt_sel": "sélectionner le fichier   ( pour couper / copier / supprimer / … )$NHotkey: S\">sel", + "tvt_edit": "ouvrir le fichier dans l'éditeur de texte$NHotkey: E\">✏️ modifier", + "tvt_tail": "surveiller le fichier pour les changements; montrer les nouvelles lignes en temps réel\">📡 suivre", + "tvt_wrap": "retour à la ligne\">↵", + "tvt_atail": "ancrer le défilement au fond de la page\">⚓", + "tvt_ctail": "décoder les couleurs du terminal (ansi escape codes)\">🌈", + "tvt_ntail": "limite de défilement en arrière (combien d'octets de texte à garder chargé)", + + "m3u_add1": "musique ajoutée à la playlist m3u", + "m3u_addn": "{0} musiques ajoutées à la playlist m3u", + "m3u_clip": "la playlist m3u est maintenant copiée dans le presse-papier\n\nvous devriez créer un nouveau fichier texte nommé par exemple playlist.m3u et coller la playlist dans ce fichier ; cela la rendra lisible en tant que playlist", + + "gt_vau": "ne pas voir les vidéos, juste jouer l'audio\">🎧", + "gt_msel": "activer la séléction de fichiers ; ctrl-clic sur un fichier pour override écraser$N$Nquand actif : double-cliquer sur un fichier / dossier pour l'ouvrir$N$NHotkey: S\">multiséléction", + "gt_crop": "rogner les miniatures au centre\">rogner", + "gt_3x": "miniatures haute résolution\">3x", + "gt_zoom": "zoomer", + "gt_chop": "rogner", + "gt_sort": "trier par", + "gt_name": "nom", + "gt_sz": "taille", + "gt_ts": "date", + "gt_ext": "type", + "gt_c1": "tronquer les noms de fichiers (montrer moins)", + "gt_c2": "tronquer les noms de fichiers (montrer plus)", + + "sm_w8": "recherche…", + "sm_prev": "les résultats de recherche ci-dessous proviennent d'une requête précédente:\n ", + "sl_close": "fermer les résultats de recherche", + "sl_hits": "affichage de {0} résultats", + "sl_moar": "chercher plus", + + "s_sz": "taille", + "s_dt": "date", + "s_rd": "chemin", + "s_fn": "nom", + "s_ta": "tags", + "s_ua": "up@", + "s_ad": "adv.", + "s_s1": "minimum MiB", + "s_s2": "maximum MiB", + "s_d1": "min. iso8601", + "s_d2": "max. iso8601", + "s_u1": "téléverser après", + "s_u2": "et/ou avant", + "s_r1": "le chemin contient   (séparé par des espaces)", + "s_f1": "le nom contient   (négation avec -nope)", + "s_t1": "les tags contiennent   (^=début, fin=$)", + "s_a1": "propriétés de métadonnées spécifiques", + + "md_eshow": "impossible d'afficher le rendu ", + "md_off": "[📜readme] disabled in [⚙️] -- document caché", + + "badreply": "Échec de l'analyse de la réponse du serveur", + + "xhr403": "403: Accès refusé\n\nessayez d'appuyer sur F5, peut-être que vous avez été déconnecté", + "xhr0": "inconnu (vous avez probablement perdu la connexion au serveur, ou le serveur est hors ligne)", + "cf_ok": "désolé pour cela -- la protection DD" + wah + "oS a été déclenché\n\nles choses devraient reprendre dans environ 30 secondes\n\nsi rien ne se passe, appuyez sur F5 pour recharger la page", + "tl_xe1": "impossible de lister les sous-dossiers:\n\nerreur ", + "tl_xe2": "404: Dossier introuvable", + "fl_xe1": "impossible de lister les fichiers dans le dossier:\n\nerreur ", + "fl_xe2": "404: Dossier introuvable", + "fd_xe1": "impossible de créer le sous-dossier:\n\nerreur ", + "fd_xe2": "404: Dossier parent introuvable", + "fsm_xe1": "impossible d'envoyer le message:\n\nerreur ", + "fsm_xe2": "404: Dossier parent introuvable", + "fu_xe1": "échec du chargement de la liste des unpost du serveur:\n\nerreur ", + "fu_xe2": "404: Fichier introuvable??", + + "fz_tar": "fichier gnu-tar non compressé (linux / mac)", + "fz_pax": "tar au format pax non compressé (plus lent)", + "fz_targz": "gnu-tar avec compression gzip niveau 3$N$Ncela est généralement très lent, donc$Nutilisez plutôt tar non compressé", + "fz_tarxz": "gnu-tar avec compression xz niveau 1$N$Ncela est généralement très lent, donc$Nutilisez plutôt tar non compressé", + "fz_zip8": "zip avec noms de fichiers utf8 (peut être instable sur windows 7 et versions antérieures)", + "fz_zipd": "zip avec noms de fichiers cp437 traditionnels, pour les très anciens logiciels", + "fz_zipc": "cp437 avec crc32 calculé tôt,$Nfor MS-DOS PKZIP v2.04g (octobre 1993)$N(prend plus de temps à charger avant que le téléchargement ne commence)", + + "un_m1": "vous pouvez supprimer vos téléchargements récents (ou annuler ceux en cours) ci-dessous", + "un_upd": "rafraîchir", + "un_m4": "ou partager les fichiers visibles ci-dessous:", + "un_ulist": "montrer", + "un_ucopy": "copier", + "un_flt": "filtre optionnel:  l'URL doit contenir", + "un_fclr": "effacer le filtre", + "un_derr": 'échec de l\'unpost-delete:\n', + "un_f5": 'quelque chose a cassé, veuillez essayer de rafraîchir ou d\'appuyer sur F5', + "un_uf5": "désolé mais vous devez rafraîchir la page (par exemple en appuyant sur F5 ou CTRL-R) avant que ce téléchargement puisse être annulé", + "un_nou": 'warning: serveur trop occupé pour afficher les téléversements non finis; cliquez sur le lien "rafraîchir" dans un instant', + "un_noc": 'warning: unpost des fichiers entièrement téléchargés n\'est pas activé/permis dans la configuration du serveur', + "un_max": "affichage des 2000 premiers fichiers (utilisez le filtre)", + "un_avail": "{0} téléchargements récents peuvent être supprimés
{1} ceux en cours peuvent être annulés", + "un_m2": "triés par date de téléchargement; les plus récents en premier:", + "un_no1": "sike! aucun téléchargement n'est suffisamment récent", + "un_no2": "sike! aucun téléchargement correspondant à ce filtre n'est suffisamment récent", + "un_next": "supprimer les {0} fichiers suivants ci-dessous", + "un_abrt": "abandonner", + "un_del": "supprimer", + "un_m3": "chargement de vos téléchargements récents…", + "un_busy": "suppression de {0} fichiers…", + "un_clip": "{0} liens copiés dans le presse-papiers", + + "u_https1": "vous devriez", + "u_https2": "passer à https", + "u_https3": "pour de meilleure performances", + "u_ancient": 'votre navigateur est impressionnamment ancien -- vous devriez peut-être utiliser bup à la place', + "u_nowork": "nécessite firefox 53+ ou chrome 57+ ou iOS 11+", + "tail_2old": "nécessite firefox 105+ ou chrome 71+ ou iOS 14.5+", + "u_nodrop": 'votre navigateur est trop ancien pour le téléversement par glisser-déposer', + "u_notdir": "ce n'est pas un dossier!\n\nvotre navigateur est trop ancien,\nveuillez essayer le glisser-déposer à la place", + "u_uri": "pour glisser-déposer des images depuis d'autres fenêtres de navigateur,\nveuillez les déposer sur le gros bouton de téléversement", + "u_enpot": 'passer à l\'interface utilisateur potato (peut améliorer la vitesse de téléversement)', + "u_depot": 'passer à l\'interface utilisateur fancy (peut réduire la vitesse de téléversement)', + "u_gotpot": 'passage à l\'interface utilisateur potato pour une vitesse de téléversement améliorée,\n\nn\'hésitez pas à revenir en arrière si ça ne vous plaît pas !', + "u_pott": "

fichiers:   {0} fini,   {1} échoué,   {2} en cours,   {3} en attente

", + "u_ever": "ceci est le téléverseur de base ; up2k nécessite au moins chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", + "u_su2k": 'ceci est le téléverseur de base; up2k est meilleur', + "u_uput": 'optimiser pour la vitesse (ignorer la somme de contrôle)', + "u_ewrite": 'vous n\'avez pas accès en écriture à ce dossier', + "u_eread": 'vous n\'avez pas accès en lecture à ce dossier', + "u_enoi": 'la recherche de fichiers n\'est pas activée dans la configuration du serveur', + "u_enoow": "l'écrasage ne fonctionnera pas ici; besoin de permissions de suppression", + "u_badf": 'Ces {0} fichiers (sur {1} au total) ont été ignorés, probablement en raison de permissions système de fichiers:\n\n', + "u_blankf": 'Ces {0} fichiers (sur {1} au total) sont vides; les téléverser quand même ?\n\n', + "u_applef": 'Ces {0} fichiers (sur {1} au total) sont probablement indésirables;\nAppuyez sur OK/Enter pour IGNORER les fichiers suivants,\nAppuyez sur Cancel/Échap pour NE PAS exclure, et TÉLÉVERSER ceux-ci également:\n\n', + "u_just1": '\nPeut-être que cela fonctionne mieux si vous sélectionnez juste un fichier', + "u_ff_many": "si vous utilisez Linux / MacOS / Android, alors ce nombre de fichiers peut faire planter Firefox!\nSi cela se produit, veuillez réessayer (ou utiliser Chrome).", + "u_up_life": "Ce téléversement va être supprimé du serveur\n{0} après son achèvement", + "u_asku": 'téléverser ces {0} fichiers vers {1}', + "u_unpt": "vous pouvez défaire / supprimer ce téléversement en utilisant le 🧯 en haut à gauche", + "u_bigtab": 'sur le point d\'afficher {0} fichiers\n\ncela peut faire planter votre navigateur, êtes-vous sûr ?', + "u_scan": 'Analyse des fichiers…', + "u_dirstuck": 'l\'itérateur de répertoire est bloqué en essayant d\'accéder aux {0} éléments suivants ; il sera ignoré :', + "u_etadone": 'Terminé ({0}, {1} fichiers)', + "u_etaprep": '(préparation au téléversement)', + "u_hashdone": 'calcul de la somme de contrôle terminé', + "u_hashing": 'calcul de la somme de contrôle', + "u_hs": 'établissement d\'une liaison…', + "u_started": "les fichiers sont maintenant en cours de téléversement ; voir [🚀]", + "u_dupdefer": "dupliqué ; sera traité après tous les autres fichiers", + "u_actx": "cliquez sur ce texte pour éviter la perte de
performance lors du passage à d'autres fenêtres/onglets", + "u_fixed": "OK!  Résolu 👍", + "u_cuerr": "echec du téléversement du morceau {0} de {1};\nprobablement inoffensif, poursuite\n\nfichier : {2}", + "u_cuerr2": "le serveur a rejeté le téléversement (morceau {0} de {1});\nréessaiera plus tard\n\nfichier : {2}\n\nerreur ", + "u_ehstmp": "réessaiera ; voir en bas à droite", + "u_ehsfin": "le serveur a rejeté la demande de finalisation du téléversement ; nouvelle tentative…", + "u_ehssrch": "le serveur a rejeté la demande d'effectuer une recherche ; nouvelle tentative…", + "u_ehsinit": "le serveur a rejeté la demande d'initier le téléversement ; nouvelle tentative…", + "u_eneths": "erreur réseau lors de l'exécution de l'initialisation du téléversement ; nouvelle tentative…", + "u_enethd": "erreur réseau lors du test de l'existence de la cible ; nouvelle tentative…", + "u_cbusy": "attente que le serveur nous fasse à nouveau confiance après un problème réseau…", + "u_ehsdf": "le serveur est à court d'espace disque !\n\nil va continuer de réessayer, au cas où quelqu'un\nlibérerait suffisamment d'espace pour continuer", + "u_emtleak1": "il semble que votre navigateur web ait une fuite de mémoire ;\nveuillez", + "u_emtleak2": ' passer à https (recommandé) ou ', + "u_emtleak3": ' ', + "u_emtleakc": 'essayez la solution suivante:\n
  • appuyez sur F5 pour rafraîchir la page
  • ensuite désactivez le bouton  mt  dans les  ⚙️ paramètres
  • et réessayez ce téléversement
Les téléversements seront un peu plus lents, mais tant pis.\nDésolé pour le dérangement !\n\nPS : chrome v107 a un correctif pour cela', + "u_emtleakf": 'essayez la solution suivante:\n
  • appuyez sur F5 pour rafraîchir la page
  • ensuite activez 🥔 (pomme de terre) dans l\'interface de téléversement
  • et réessayez ce téléversement
\nPS : firefox aura probablement un correctif à un moment donné', + "u_s404": "pas trouvé sur le serveur", + "u_expl": "expliquer", + "u_maxconn": "la plupart des navigateur limite ceci à 6, mais firefox vous permet de l'augmenter avec connections-per-server dans about:config", + "u_tu": '

WARNING: turbo enclenché,  le client peut ne pas détecter et reprendre les téléversements incomplets ; voir l\'info-bulle du bouton turbo

', + "u_ts": '

WARNING: turbo enclenché,  les résultats de recherche peuvent être incorrects ; voir l\'info-bulle du bouton turbo

', + "u_turbo_c": "turbo est désactivé dans la configuration du serveur", + "u_turbo_g": "désactivation de turbo car vous n'avez pas de\nprivilèges de listing de répertoires dans ce volume", + "u_life_cfg": 'suppression automatique après min (ou heures)', + "u_life_est": 'le téléversement sera supprimé ---', + "u_life_max": 'ce dossier impose une\ndurée de vie maximale de {0}', + "u_unp_ok": 'unpost est autorisé pour {0}', + "u_unp_ng": 'unpost ne sera PAS autorisé', + "ue_ro": 'votre accès à ce dossier est en lecture seule\n\n', + "ue_nl": 'vous n\'êtes actuellement pas connecté', + "ue_la": 'vous êtes actuellement connecté en tant que "{0}"', + "ue_sr": 'vous êtes actuellement en mode recherche de fichiers\n\nchangez en mode téléversement en cliquant sur la loupe 🔎 (à côté du grand bouton RECHERCHER), et essayez de téléverser à nouveau\n\ndésolé', + "ue_ta": 'essayez de téléverser à nouveau, cela devrait fonctionner maintenant', + "ue_ab": "ce fichier a déjà été téléversé dans un autre dossier, et ce téléversement doit être terminé avant que le fichier puisse être téléversé ailleurs.\n\nVous pouvez annuler et oublier le téléversement initial en utilisant le bouton 🧯 en haut à gauche.", + "ur_1uo": "OK: Fichier téléversé avec succès", + "ur_auo": "OK: Tous les {0} fichiers téléversés avec succès", + "ur_1so": "OK: Fichier trouvé sur le serveur", + "ur_aso": "OK: Tous les {0} fichiers trouvés sur le serveur", + "ur_1un": "Échec du téléversement, désolé", + "ur_aun": "Tous les {0} téléversements ont échoué, désolé", + "ur_1sn": "Fichier NON trouvé sur le serveur", + "ur_asn": "Les {0} fichiers n'ont PAS ÉTÉ trouvés sur le serveur", + "ur_um": "Terminé;\n{0} téléversements OK,\n{1} téléversements échoués, désolé", + "ur_sm": "Terminé;\n{0} fichiers trouvés sur le serveur,\n{1} fichiers NON trouvés sur le serveur", + + "lang_set": "rafraîchir pour que les changements prennent effet ?", + }, "grc": { "tt": "Ελληνικά", @@ -8816,7 +9445,7 @@ var Ls = { }, }; -var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "grc", "ita", "nld", "rus", "spa", "ukr"]; +var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "nld", "rus", "spa", "ukr"]; if (window.langmod) langmod(); From 074e106e24afe5cf2cac100d82a78c872446de9a Mon Sep 17 00:00:00 2001 From: icxes Date: Sun, 10 Aug 2025 12:17:00 +0300 Subject: [PATCH 005/154] fix PRTY_CONFIG not reading global flags from the config --- copyparty/__main__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index e87fe487..8c630115 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1767,6 +1767,10 @@ def main(argv: Optional[list[str]] = None) -> None: ensure_webdeps() + if CFG_DEF: + supp = args_from_cfg(CFG_DEF[0]) + argv.extend(supp) + for k, v in zip(argv[1:], argv[2:]): if k == "-c" and os.path.isfile(v): supp = args_from_cfg(v) From 7aa21483c5789681a4796def4e9ba7f461035358 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Sun, 10 Aug 2025 06:06:23 -0400 Subject: [PATCH 006/154] French translation part 2: splash.js #553 --- copyparty/web/browser.js | 2 +- copyparty/web/splash.js | 53 +++++++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index f6b1e3eb..9739e55a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -3893,7 +3893,7 @@ var Ls = { "gou": 'dossier parent">haut', "gon": 'dossier suivant">suivant', "logout": "Déconnexion ", - "access": "accès", + "access": " accès", "ot_close": "fermer le sous-menu", "ot_search": "chercher des fichiers par leurs attributs, chemin / nom, tag musicaux, ou nimporte quelle combinaison de ces options$N$N<code>foo bar</code> = doit contenir à la fois «foo» et «bar»,$N<code>foo -bar</code> = doit contenir «foo» mais pas «bar»,$N<code>^yana .opus$</code> = commence par «yana» et est un fichier «opus»$N<code>"try unite"</code> = contient exactement «try unite»$N$Nle format de date est iso-8601, comme$N<code>2009-12-31</code> ou <code>2020-09-12 23:30:00</code>", "ot_unpost": "unpost: supprimer vos téléchargements récents, ou annuler ceux en cours", diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 2475cb97..d6c3a97f 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -98,33 +98,33 @@ var Ls = { "a1": "obnovit", "b1": "ahoj cizinče   (nejsi přihlášen)", "c1": "odhlásit se", - "d1": "vypsat zásobníku", // TLNote: "d2" is the tooltip for this button + "d1": "vypsat zásobníku", "d2": "zobrazit stav všech aktivních vláken", "e1": "znovu načíst konfiguraci", "e2": "znovu načíst konfigurační soubory (accounts/volumes/volflags),$Na prohledat všechny e2ds úložiště$N$Npoznámka: všechny změny globálních nastavení$Nvyžadují úplné restartování, aby se projevily", "f1": "můžeš procházet:", "g1": "můžeš nahrávat do:", "cc1": "další věci:", - "h1": "zakázat k304", // TLNote: "j1" explains what k304 is + "h1": "zakázat k304", "i1": "povolit k304", "j1": "povolení k304 odpojí vašeho klienta při každém HTTP 304, což může zabránit některým chybovým proxy serverům, aby se zasekly (náhle nenačítaly stránky), ale také to obecně zpomalí věci", "k1": "resetovat nastavení klienta", "l1": "přihlaste se pro více:", - "m1": "vítej zpět,", // TLNote: "welcome back, USERNAME" + "m1": "vítej zpět,", "n1": "404 nenalezeno  ┐( ´ -`)┌", "o1": 'nebo možná nemáš přístup -- zkus heslo nebo jdi domů', "p1": "403 zakázáno  ~┻━┻", "q1": 'použij heslo nebo jdi domů', "r1": "jdi domů", ".s1": "znovu prohledat", - "t1": "akce", // TLNote: this is the header above the "rescan" buttons + "t1": "akce", "u2": "čas od posledního zápisu na server$N( upload / rename / ... )$N$N17d = 17 dní$N1h23 = 1 hodina 23 minut$N4m56 = 4 minuty 56 sekund", "v1": "připojit", "v2": "použít tento server jako místní HDD", "w1": "přepnout na https", "x1": "změnit heslo", - "y1": "upravit sdílení", // TLNote: shows the list of folders that the user has decided to share - "z1": "odblokovat toto sdílení:", // TLNote: the password prompt to see a hidden share + "y1": "upravit sdílení", + "z1": "odblokovat toto sdílení:", "ta1": "nejprve vyplňte své nové heslo", "ta2": "zopakujte pro potvrzení nového hesla:", "ta3": "nalezen překlep; zkuste to prosím znovu", @@ -218,6 +218,47 @@ var Ls = { "af1": "näytä viimeaikaiset lataukset", "ag1": "näytä tunnetut IdP-käyttäjät", }, + "fra": { + "a1": "rafraîchir", + "b1": "salut étranger   (vous n'êtes pas connecté.)", + "c1": "déconnexion", + "d1": "vidange de la pile", + "d2": "affiche l'état de tous les threads actifs", + "e1": "recharger la configuration", + "e2": "recharger le fichier configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les e2ds volumes$N$Nnote: n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet", + "f1": "vous pouvez naviguer:", + "g1": "vous pouvez télécharger sur:", + "cc1": "autres choses:", + "h1": "désactiver k304", + "i1": "activer k304", + "j1": "activer k304 vouloir déconnecter votre client sur chaque HTTP 304, ce qui peut éviter certains proxys défectueux d'avoir bloqué (les pages ne se chargent soudainement plus), mais cela ralentira également les choses en général", + "k1": "réinitialiser client paramètres", + "l1": "connectez-vous pour en savoir plus:", + "m1": "heureux vous revoir,", + "n1": "404 introuvable  ┐( ´ -`)┌", + "o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou aller à la page d\'accueil', + "p1": "403 interdita  ~┻━┻", + "q1": 'utiliser un mot de passe or aller à la page d\'accueil', + "r1": "aller à la page d\'accueil", + ".s1": "rescan", + "t1": "action", + "u2": "temps écoulé depuis la dernière écriture sur le serveur$N( télécharger / renommer / ... )$N$N17j = 17 jours$N1h23 = 1 heur 23 minutes$N4m56 = 4 minutes 56 secondes", + "v1": "connecter", + "v2": "utilisez ce serveur en tant que disque dur local", + "w1": "passer à https", + "x1": "changer mot de passe", + "y1": "modifier les partages", + "z1": "déverrouiller ce partage:", + "ta1": "entrez d'abord votre nouveau mot de passe", + "ta2": "répétez pour confirmer le nouveau mot de passe:", + "ta3": "une faute de frappe a été détectée ; veuillez réessayer.", + "aa1": "fichiers entrants:", + "ab1": "désactiver no304", + "ac1": "activer no304", + "ad1": "l'activation de no304 désactivera toute mise en cache; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable!", + "ae1": "téléchargements actifs:", + "af1": "afficher les derniers téléchargements", + }, "grc": { "a1": "ανανέωση", "b1": "γεια σου ξένε!   (δεν είσαι συνδεδεμένος)", From 3c78c6a880807d78efa754aeb7a559d99d3b6b57 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 10:03:42 +0000 Subject: [PATCH 007/154] custom mdns domain, closes #549 --- copyparty/__main__.py | 1 + copyparty/mdns.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 8c630115..5c2771fe 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1201,6 +1201,7 @@ def add_zc_mdns(ap): ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares") ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares") ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares") + ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems") ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network") ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not") ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty") diff --git a/copyparty/mdns.py b/copyparty/mdns.py index a9d36fe7..0dde707d 100644 --- a/copyparty/mdns.py +++ b/copyparty/mdns.py @@ -76,7 +76,8 @@ class MDNS(MCast): if not self.args.zm_nwa_1: set_avahi_379() - zs = self.args.name + ".local." + zs = self.args.zm_fqdn or (self.args.name + ".local") + zs = zs.replace("--name", self.args.name).rstrip(".") + "." zs = zs.encode("ascii", "replace").decode("ascii", "replace") self.hn = "-".join(x for x in zs.split("?") if x) or ( "vault-{}".format(random.randint(1, 255)) From 8ba98877ee15af62bac082e28248ef0feb8c6ee6 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 11:23:22 +0000 Subject: [PATCH 008/154] patch pyftpdlib, fixes #539 upgrading pyftpdlib brings only pain and no benefits so grafting a patch for this instead --- scripts/make-sfx.sh | 1 + scripts/patches/pyftpdlib-win313.patch | 41 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 scripts/patches/pyftpdlib-win313.patch diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index a1ee1a06..f752f8a8 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -217,6 +217,7 @@ necho() { tar -zxf $f mv pyftpdlib-*/pyftpdlib . rm -rf pyftpdlib-* pyftpdlib/test + patch -p1 <../scripts/patches/pyftpdlib-win313.patch for f in pyftpdlib/_async{hat,ore}.py; do [ -e "$f" ] || continue; iawk 'NR<4||NR>27||!/^#/;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' $f diff --git a/scripts/patches/pyftpdlib-win313.patch b/scripts/patches/pyftpdlib-win313.patch new file mode 100644 index 00000000..62601108 --- /dev/null +++ b/scripts/patches/pyftpdlib-win313.patch @@ -0,0 +1,41 @@ +Date: Tue, 22 Oct 2024 12:47:30 +0200 +Subject: Workaround for isabs() on Windows + Python 3.13 (#652) + +Starting from Python 3.13, `os.path.isabs("/foo")` on Windows return `False` + +diff --git a/pyftpdlib/filesystems.py b/pyftpdlib/filesystems.py +index 9b9326bf..320ffe40 100644 +--- a/pyftpdlib/filesystems.py ++++ b/pyftpdlib/filesystems.py +@@ -132,6 +132,16 @@ def cwd(self, path): + + # --- Pathname / conversion utilities + ++ @staticmethod ++ def _isabs(path, _windows=os.name == "nt"): ++ # Windows + Python 3.13: isabs() changed so that a path ++ # starting with "/" is no longer considered absolute. ++ # https://github.com/python/cpython/issues/44626 ++ # https://github.com/python/cpython/pull/113829/ ++ if _windows and path.startswith("/"): ++ return True ++ return os.path.isabs(path) ++ + def ftpnorm(self, ftppath): + """Normalize a "virtual" ftp pathname (typically the raw string + coming from client) depending on the current working directory. +@@ -146,3 +156,3 @@ + assert isinstance(ftppath, unicode), ftppath +- if os.path.isabs(ftppath): ++ if self._isabs(ftppath): + p = os.path.normpath(ftppath) +@@ -162,3 +172,3 @@ + # This is for extra protection, maybe not really necessary. +- if not os.path.isabs(p): ++ if not self._isabs(p): + p = u("/") +@@ -201,3 +211,3 @@ + assert isinstance(fspath, unicode), fspath +- if os.path.isabs(fspath): ++ if self._isabs(fspath): + p = os.path.normpath(fspath) From 347cf6a5461d281ef90935d1e9023d95b909230c Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 11:28:22 +0000 Subject: [PATCH 009/154] fix dropdown color --- copyparty/web/browser.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index b5506f39..298de0ea 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1377,7 +1377,7 @@ html.y #ops svg circle { .opview select, .opview input[type=text] { color: var(--fg); - background: var(--txt-bg); + background: var(--bg-u5); border: none; box-shadow: 0 0 2px var(--txt-sh); border-bottom: 1px solid #999; From e5e822951d66f5a9b60e3f4a22ffa6b1a46f6b43 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 11:31:12 +0000 Subject: [PATCH 010/154] fix filter case-sensitivity --- copyparty/httpcli.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 402d623f..b981172d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5481,13 +5481,13 @@ class HttpCli(object): q = "select sz, rd, fn, at from up where ip=? and at>? order by at desc" for sz, rd, fn, at in cur.execute(q, (self.ip, lim)): vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) - if nfi == 0 or (nfi == 1 and vfi in vp): + if nfi == 0 or (nfi == 1 and vfi in vp.lower()): pass elif nfi == 2: - if not vp.startswith(vfi): + if not vp.lower().startswith(vfi): continue elif nfi == 3: - if not vp.endswith(vfi): + if not vp.lower().endswith(vfi): continue else: continue @@ -5607,13 +5607,13 @@ class HttpCli(object): q = "select sz, rd, fn, ip, at from up where at>0 order by at desc" for sz, rd, fn, ip, at in cur.execute(q): vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) - if nfi == 0 or (nfi == 1 and vfi in vp): + if nfi == 0 or (nfi == 1 and vfi in vp.lower()): pass elif nfi == 2: - if not vp.startswith(vfi): + if not vp.lower().startswith(vfi): continue elif nfi == 3: - if not vp.endswith(vfi): + if not vp.lower().endswith(vfi): continue else: continue From 03acd65e9666c456703a3c73b809556b93577291 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 11:45:20 +0000 Subject: [PATCH 011/154] avoid ios bug (keystore spam) --- copyparty/web/splash.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index d6c3a97f..08551999 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -625,7 +625,7 @@ catch (ex) { } tt.init(); var o = QS('input[name="uname"]') || QS('input[name="cppwd"]'); -if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) +if (!MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) o.focus(); o = ebi('u'); From 3560eeb10eed1ae6641f6855f8cd1f5e77f8746b Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 13:55:29 +0200 Subject: [PATCH 012/154] better with sessions --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index b981172d..29e4c610 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -4971,7 +4971,7 @@ class HttpCli(object): rip = host vp = (self.uparam["hc"] or "").lstrip("/") - pw = self.pw or "hunter2" + pw = self.ouparam.get("pw") or "hunter2" if pw in self.asrv.sesa: pw = "hunter2" From a01870b744dc12292808f2002ad86b1e7f1268f8 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 13:55:52 +0200 Subject: [PATCH 013/154] avoid macos bug (finder hangs on connect) --- copyparty/httpcli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 29e4c610..72bff45e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1599,6 +1599,10 @@ 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 b87f8f1b01dc9e53005bbe9ec004c66fd25b34c8 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 12:10:54 +0000 Subject: [PATCH 014/154] french improvements by @Equinoxs #553 --- copyparty/web/splash.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 08551999..8cbca1e6 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -225,38 +225,38 @@ var Ls = { "d1": "vidange de la pile", "d2": "affiche l'état de tous les threads actifs", "e1": "recharger la configuration", - "e2": "recharger le fichier configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les e2ds volumes$N$Nnote: n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet", - "f1": "vous pouvez naviguer:", - "g1": "vous pouvez télécharger sur:", - "cc1": "autres choses:", + "e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet", + "f1": "vous pouvez naviguer :", + "g1": "vous pouvez télécharger sur :", + "cc1": "autres choses :", "h1": "désactiver k304", "i1": "activer k304", - "j1": "activer k304 vouloir déconnecter votre client sur chaque HTTP 304, ce qui peut éviter certains proxys défectueux d'avoir bloqué (les pages ne se chargent soudainement plus), mais cela ralentira également les choses en général", - "k1": "réinitialiser client paramètres", - "l1": "connectez-vous pour en savoir plus:", - "m1": "heureux vous revoir,", + "j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), mais cela ralentira également les choses en général", + "k1": "réinitialiser les paramètres du client", + "l1": "connectez-vous pour en savoir plus :", + "m1": "heureux de vous revoir,", "n1": "404 introuvable  ┐( ´ -`)┌", "o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou aller à la page d\'accueil', - "p1": "403 interdita  ~┻━┻", - "q1": 'utiliser un mot de passe or aller à la page d\'accueil', + "p1": "403 interdit  ~┻━┻", + "q1": 'utiliser un mot de passe ou aller à la page d\'accueil', "r1": "aller à la page d\'accueil", - ".s1": "rescan", + ".s1": "rescanner", "t1": "action", - "u2": "temps écoulé depuis la dernière écriture sur le serveur$N( télécharger / renommer / ... )$N$N17j = 17 jours$N1h23 = 1 heur 23 minutes$N4m56 = 4 minutes 56 secondes", + "u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes", "v1": "connecter", "v2": "utilisez ce serveur en tant que disque dur local", "w1": "passer à https", "x1": "changer mot de passe", "y1": "modifier les partages", - "z1": "déverrouiller ce partage:", + "z1": "déverrouiller ce partage :", "ta1": "entrez d'abord votre nouveau mot de passe", - "ta2": "répétez pour confirmer le nouveau mot de passe:", + "ta2": "répétez pour confirmer le nouveau mot de passe :", "ta3": "une faute de frappe a été détectée ; veuillez réessayer.", - "aa1": "fichiers entrants:", + "aa1": "fichiers entrants :", "ab1": "désactiver no304", "ac1": "activer no304", - "ad1": "l'activation de no304 désactivera toute mise en cache; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable!", - "ae1": "téléchargements actifs:", + "ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !", + "ae1": "téléchargements actifs :", "af1": "afficher les derniers téléchargements", }, "grc": { From c2cee222bdc3ae836ca1adb52f2af6381e8a5a1f Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 12:26:12 +0000 Subject: [PATCH 015/154] v1.19.1 --- copyparty/__main__.py | 6 +++--- copyparty/__version__.py | 4 ++-- docs/changelog.md | 46 ++++++++++++++++++++++++++++++++++++++++ scripts/rls.sh | 2 ++ scripts/tl.js | 1 + 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 5c2771fe..2468afe6 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1391,7 +1391,7 @@ def add_shutdown(ap): 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, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") + 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("--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") @@ -1494,8 +1494,8 @@ def add_db_general(ap, hcores): ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty") ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)") ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)") - ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)") - ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)") + ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)") + ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)") ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)") ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch") ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower") diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 3350f413..1cea26c5 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 0) +VERSION = (1, 19, 1) CODENAME = "usernames" -BUILD_DT = (2025, 8, 7) +BUILD_DT = (2025, 8, 10) 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 a3c0aaae..91fd1b84 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,49 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-0807-2213 `v1.19.0` usernames + +## 🧪 new features + +* #511 login with username and password (not just password) can now optionally be enabled with `--usernames` 346515cc + * if you have enabled password hashing (`ah-alg: argon2` or similar) then you will need to hash your passwords again after enabling usernames, hashing them as `username:password:` +* #468 add Greek translation (thx @chamdim!) 50f46187 392abd06 +* #471 add Czech translation (thx @kubakubakuba!) c9556583 +* #515 support systemd socket acivation (thx @mati1210!) 9b9d2a92 +* #523 add QR-code to the connectpage bcc3b156 +* #513 optional EOL-conversion for texteditor 8b31ed88 +* controlpanel refresh-button now toggles automatic refresh 7ae84dea + +## 🩹 bugfixes + +* fix stuck uploads when the up2k database (`e2d`) is not enabled 4a043568 + * if more than 60'000 files were uploaded and there were several dupes of some files, they could get stuck and never upload + * upload performance is improved remarkably by enabling `e2d` so such huge uploads non-e2d had not been tested in a long time +* #467 #470 fix ui-crash when exporting links of all uploaded files to clipboard (thx @geekalaa!) 0df1901f +* #487 fix ui-crash when the location url-part is `//` 0f55a1ae +* fix viewing `.MD` files (8a0746c6) + +## 🔧 other changes + +* when a reverse-proxy is detected, force explicit configuration of `--rproxy` to obtain correct client IP 3f8cb7e8 + * a bit inconvenient, but helps prevent potentially-dangerous misconfiguration + * the necessary configuration changes are explained in the serverlog (you can't miss it) + * thanks to @person4268 for pointing out that there was room for improvements! +* failed login attempts now only log a sha512 hash of the provided password + * to see login-attempts with incorrect passwords as plaintext like before, `log-badpwd: 1` +* #502 add systemd user services and templated services (thx @icxes!) 34d98e99 +* #475 improve helptext for multivalue global-options c2ac57a2 +* #475 add [chungus.conf](https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf), massive extensive nonsensical demo config b664ebb0 +* try to detect proxies with incorrect caching behavior 9e980bb5 +* recent-uploads now support ie9 a57f7cc2 +* languages and themes are now dropdowns a9ee4f24 +* copyparty.exe: upgrade python to 3.13.6 a98360f2 +* introduce [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py), english-only edition of copyparty-sfx.py to save space 33497e6b + +## 🗿 known issues + +* the `copyparty.pyz` in this release is english-only, and does not include the translations -- they got lost in transit while adjusting the buildscripts to make `copyparty-en.py` + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-0804-0013 `v1.18.10` idp speedboost diff --git a/scripts/rls.sh b/scripts/rls.sh index 767ca2de..d18f12f2 100755 --- a/scripts/rls.sh +++ b/scripts/rls.sh @@ -43,6 +43,8 @@ while [ "$1" ]; do shift done +./make-pyz.sh + ./make-sfx.sh re lang eng "$@" mv ../dist/copyparty-{sfx,en}.py mv ../dist/copyparty-{int,sfx}.py diff --git a/scripts/tl.js b/scripts/tl.js index 97bf061d..4d4fcaba 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -81,6 +81,7 @@ var tl_cpanel = { "ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!", "ae1": "active downloads:", "af1": "show recent uploads", + "ag1": "view idp cache", }, }; From db2a03409c7bdbcd0752e4b97dad2139db5c77ef Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 10 Aug 2025 12:44:51 +0000 Subject: [PATCH 016/154] update pkgs to 1.19.1 --- 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 e5896fd7..3c6a4abb 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.19.0" +pkgver="1.19.1" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("179b027d51e4fe7ebdab2b18c07475d52c57e2ce69256292b157a8efacd82118") +sha256sums=("bbc250db23eb80bc96c27b2efa456ce1e7f49c7dfaabadb91a571f70064b6f91") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 645623c0..6201430a 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.0 +pkgver=1.19.1 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("179b027d51e4fe7ebdab2b18c07475d52c57e2ce69256292b157a8efacd82118") +sha256sums=("bbc250db23eb80bc96c27b2efa456ce1e7f49c7dfaabadb91a571f70064b6f91") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index c0d8c68a..55f1c2e1 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.19.0/copyparty-sfx.py", - "version": "1.19.0", - "hash": "sha256-9A+zPtkVtUuGHB/JJV3fhVtJderLUGxHqvuJQz0/1+Q=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.1/copyparty-sfx.py", + "version": "1.19.1", + "hash": "sha256-Orn0N//DD5/5rIWK9yYRcvyOnt/bKCE9CeoxkfNO76s=" } \ No newline at end of file From 0da93659a4def6c3adbba71c4887d368a7e3a1cd Mon Sep 17 00:00:00 2001 From: Bevinsky Date: Sun, 10 Aug 2025 17:19:43 +0200 Subject: [PATCH 017/154] Add missing translated string in up2k. --- copyparty/web/up2k.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index 2743ea36..6ae7046f 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -2831,7 +2831,7 @@ function up2k_init(subtle) { if (!t.t_uploading) t.t_uploading = Date.now(); - pvis.seth(t.n, 1, "🚀 send"); + pvis.seth(t.n, 1, "🚀 " + L.ul_send); var chunksize = get_chunksize(t.size), car = pcar * chunksize, From 9435e6b2e2d3fb3a32fcba7abc3b755a840ec5a6 Mon Sep 17 00:00:00 2001 From: AppleTheGolden Date: Sun, 10 Aug 2025 22:30:37 +0200 Subject: [PATCH 018/154] EPUB Thumbnailing support (#561) * EPUB Thumbnailing support --------- Signed-off-by: ed Co-authored-by: ed --- copyparty/__main__.py | 6 ++-- copyparty/dxml.py | 3 ++ copyparty/mtag.py | 73 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 2468afe6..be7c9979 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1443,13 +1443,13 @@ def add_thumbnail(ap): # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://github.com/libvips/libvips # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' - ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") + ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") - ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") + ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)") - ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg") + ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg") def add_transcoding(ap): diff --git a/copyparty/dxml.py b/copyparty/dxml.py index b49f060b..6271da8e 100644 --- a/copyparty/dxml.py +++ b/copyparty/dxml.py @@ -65,6 +65,9 @@ DXMLParser = _DXMLParser def parse_xml(txt: str) -> ET.Element: + """ + Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts. + """ parser = DXMLParser() parser.feed(txt) return parser.close() # type: ignore diff --git a/copyparty/mtag.py b/copyparty/mtag.py index e9428b79..660f8180 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -29,7 +29,7 @@ from .util import ( ) if True: # pylint: disable=using-constant-test - from typing import Any, Optional, Union + from typing import IO, Any, Optional, Union from .util import NamedLogger, RootLogger @@ -176,6 +176,9 @@ def au_unpk( raise Exception("no images inside cbz") fi = zf.open(using) + elif pk == "epub": + fi = get_cover_from_epub(log, abspath) + else: raise Exception("unknown compression %s" % (pk,)) @@ -365,6 +368,74 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ return zd, md +def get_cover_from_epub(log: "NamedLogger", abspath: str) -> IO[bytes] | None: + import zipfile + + from .dxml import parse_xml + + try: + from urlparse import urljoin # Python2 + except ImportError: + from urllib.parse import urljoin # Python3 + + with zipfile.ZipFile(abspath, "r") as z: + # First open the container file to find the package document (.opf file) + try: + container_root = parse_xml(z.read("META-INF/container.xml").decode()) + except KeyError: + log("epub: no container file found in %s" % (abspath,)) + return None + + # https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem + container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"} + # One file could contain multiple package documents, default to the first one + rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get( + "full-path" + ) + + # Then open the first package document to find the path of the cover image + try: + package_root = parse_xml(z.read(rootfile_path).decode()) + except KeyError: + log("epub: no package document found in %s" % (abspath,)) + return None + + # https://www.w3.org/TR/epub-33/#sec-package-doc + package_ns = {"": "http://www.idpf.org/2007/opf"} + # https://www.w3.org/TR/epub-33/#sec-cover-image + coverimage_path_node = package_root.find( + "./manifest/item[@properties='cover-image']", package_ns + ) + if coverimage_path_node is not None: + coverimage_path = coverimage_path_node.get("href") + else: + # This might be an EPUB2 file, try the legacy way of specifying covers + coverimage_path = _get_cover_from_epub2(log, package_root, package_ns) + + # This url is either absolute (in the .epub) or relative to the package document + adjusted_cover_path = urljoin(rootfile_path, coverimage_path) + + return z.open(adjusted_cover_path) + + +def _get_cover_from_epub2(log: "NamedLogger", package_root, package_ns) -> str | None: + # in , then + # in + cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get( + "content" + ) + + if not cover_id: + return None + + for node in package_root.iterfind("./manifest/item", package_ns): + if node.get("id") == cover_id: + cover_path = node.get("href") + return cover_path + + return None + + class MTag(object): def __init__(self, log_func: "RootLogger", args: argparse.Namespace) -> None: self.log_func = log_func From 0177a9b4025164b9d961f65fd71a49077817d8a2 Mon Sep 17 00:00:00 2001 From: "Adam R. Nelson" Date: Mon, 11 Aug 2025 13:28:01 -0400 Subject: [PATCH 019/154] Add RAW file thumbnailing support via rawpy (#567) * add RAW image file types to mimetype list * add RAW thumbnailer via rawpy --------- Signed-off-by: Adam R. Nelson Signed-off-by: ed --- README.md | 3 + copyparty/__main__.py | 4 +- copyparty/svchub.py | 4 ++ copyparty/th_cli.py | 6 +- copyparty/th_srv.py | 127 +++++++++++++++++++++++++++++++++--------- copyparty/util.py | 3 + docs/chungus.conf | 5 +- 7 files changed, 122 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 8f470a50..b3a2d134 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ also see [comparison to similar software](./docs/versus.md) * ☑ realtime streaming of growing files (logfiles and such) * ☑ [thumbnails](#thumbnails) * ☑ ...of images using Pillow, pyvips, or FFmpeg + * ☑ ...of RAW images using rawpy * ☑ ...of videos using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg * ☑ cache eviction (max-age; maybe max-size eventually) @@ -2795,6 +2796,7 @@ enable [thumbnails](#thumbnails) of... * **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler) * **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+ * **JPEG XL pictures:** `pyvips` or `ffmpeg` +* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats) enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq` @@ -2828,6 +2830,7 @@ set any of the following environment variables to disable its associated optiona | `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pyheif-pillow-opener/) | | `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow | | `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows | +| `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images | | `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg | example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py` diff --git a/copyparty/__main__.py b/copyparty/__main__.py index be7c9979..946b3d0a 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1431,7 +1431,7 @@ def add_thumbnail(ap): ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") - ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference") + ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference") ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)") @@ -1442,9 +1442,11 @@ def add_thumbnail(ap): ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://github.com/libvips/libvips + # https://stackoverflow.com/a/47612661 # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") + ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 1af13276..5e0ab886 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -40,6 +40,7 @@ from .th_srv import ( HAVE_PIL, HAVE_VIPS, HAVE_WEBP, + HAVE_RAW, ThumbSrv, ) from .up2k import Up2k @@ -321,6 +322,8 @@ class SvcHub(object): decs.pop("vips", None) if not HAVE_PIL: decs.pop("pil", None) + if not HAVE_RAW: + decs.pop("raw", None) if not HAVE_FFMPEG or not HAVE_FFPROBE: decs.pop("ff", None) @@ -811,6 +814,7 @@ class SvcHub(object): (HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"), (HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"), (HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"), + (HAVE_RAW, "rawpy", "read RAW images"), ] if ANYWIN: to_check += [ diff --git a/copyparty/th_cli.py b/copyparty/th_cli.py index 6c2632b5..384316ee 100644 --- a/copyparty/th_cli.py +++ b/copyparty/th_cli.py @@ -36,11 +36,15 @@ class ThumbCli(object): if not c: raise Exception() except: - c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]} + c = { + k: set() + for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"] + } self.thumbable = c["thumbable"] self.fmt_pil = c["pil"] self.fmt_vips = c["vips"] + self.fmt_raw = c["raw"] self.fmt_ffi = c["ffi"] self.fmt_ffv = c["ffv"] self.fmt_ffa = c["ffa"] diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 7b07376a..b37591c6 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -2,6 +2,7 @@ from __future__ import print_function, unicode_literals import hashlib +import io import logging import os import re @@ -121,6 +122,17 @@ try: except: HAVE_VIPS = False +try: + if os.environ.get("PRTY_NO_RAW"): + raise Exception() + + HAVE_RAW = True + import rawpy + + logging.getLogger("rawpy").setLevel(logging.WARNING) +except: + HAVE_RAW = False + th_dir_cache = {} @@ -205,11 +217,19 @@ class ThumbSrv(object): if self.args.th_clean: Daemon(self.cleaner, "thumb.cln") - self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [ + ( + self.fmt_pil, + self.fmt_vips, + self.fmt_raw, + self.fmt_ffi, + self.fmt_ffv, + self.fmt_ffa, + ) = [ set(y.split(",")) for y in [ self.args.th_r_pil, self.args.th_r_vips, + self.args.th_r_raw, self.args.th_r_ffi, self.args.th_r_ffv, self.args.th_r_ffa, @@ -232,6 +252,9 @@ class ThumbSrv(object): if "vips" in self.args.th_dec: self.thumbable |= self.fmt_vips + if "raw" in self.args.th_dec: + self.thumbable |= self.fmt_raw + if "ff" in self.args.th_dec: for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]: self.thumbable |= zss @@ -313,6 +336,7 @@ class ThumbSrv(object): "thumbable": self.thumbable, "pil": self.fmt_pil, "vips": self.fmt_vips, + "raw": self.fmt_raw, "ffi": self.fmt_ffi, "ffv": self.fmt_ffv, "ffa": self.fmt_ffa, @@ -368,6 +392,8 @@ class ThumbSrv(object): funs.append(self.conv_pil) elif lib == "vips" and ext in self.fmt_vips: funs.append(self.conv_vips) + elif lib == "raw" and ext in self.fmt_raw: + funs.append(self.conv_raw) elif can_au and (want_png or want_au): if want_opus: funs.append(self.conv_opus) @@ -480,35 +506,38 @@ class ThumbSrv(object): return im + def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None: + try: + im = self.fancy_pillow(im, fmt, vn) + except Exception as ex: + self.log("fancy_pillow {}".format(ex), "90") + im.thumbnail(self.getres(vn, fmt)) + + fmts = ["RGB", "L"] + args = {"quality": 40} + + if tpath.endswith(".webp"): + # quality 80 = pillow-default + # quality 75 = ffmpeg-default + # method 0 = pillow-default, fast + # method 4 = ffmpeg-default + # method 6 = max, slow + fmts.extend(("RGBA", "LA")) + args["method"] = 6 + else: + # default q = 75 + args["progressive"] = True + + if im.mode not in fmts: + # print("conv {}".format(im.mode)) + im = im.convert("RGB") + + im.save(tpath, **args) + def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) with Image.open(fsenc(abspath)) as im: - try: - im = self.fancy_pillow(im, fmt, vn) - except Exception as ex: - self.log("fancy_pillow {}".format(ex), "90") - im.thumbnail(self.getres(vn, fmt)) - - fmts = ["RGB", "L"] - args = {"quality": 40} - - if tpath.endswith(".webp"): - # quality 80 = pillow-default - # quality 75 = ffmpeg-default - # method 0 = pillow-default, fast - # method 4 = ffmpeg-default - # method 6 = max, slow - fmts.extend(("RGBA", "LA")) - args["method"] = 6 - else: - # default q = 75 - args["progressive"] = True - - if im.mode not in fmts: - # print("conv {}".format(im.mode)) - im = im.convert("RGB") - - im.save(tpath, **args) + self.conv_image_pil(im, tpath, fmt, vn) def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) @@ -531,6 +560,50 @@ class ThumbSrv(object): assert img # type: ignore # !rm img.write_to_file(tpath, Q=40) + def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: + self.wait4ram(0.2, tpath) + with rawpy.imread(abspath) as raw: + thumb = raw.extract_thumb() + if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"): + # if we have a jpg thumbnail and no webp output is available, + # just write the jpg directly (it'll be the wrong size, but it's fast) + with open(tpath, "wb") as f: + f.write(thumb.data) + if HAVE_VIPS: + crops = ["centre", "none"] + if "f" in fmt: + crops = ["none"] + w, h = self.getres(vn, fmt) + kw = {"height": h, "size": "down", "intent": "relative"} + + for c in crops: + try: + kw["crop"] = c + if thumb.format == rawpy.ThumbFormat.BITMAP: + img = pyvips.Image.new_from_array( + thumb.data, interpretation="rgb" + ) + img = img.thumbnail_image(w, **kw) + else: + img = pyvips.Image.thumbnail_buffer(thumb.data, w, **kw) + break + except: + if c == crops[-1]: + raise + + assert img # type: ignore # !rm + img.write_to_file(tpath, Q=40) + elif HAVE_PIL: + if thumb.format == rawpy.ThumbFormat.BITMAP: + im = Image.fromarray(thumb.data, "RGB") + else: + im = Image.open(io.BytesIO(thumb.data)) + self.conv_image_pil(im, tpath, fmt, vn) + else: + raise Exception( + "either pil or vips is needed to process embedded bitmap thumbnails in raw files" + ) + def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) diff --git a/copyparty/util.py b/copyparty/util.py index b5a2c45e..1578d291 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -399,6 +399,9 @@ application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=v text ass=plain ssa=plain image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu image heic=heic-sequence heif=heif-sequence hdr=vnd.radiance svg=svg+xml +image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf +image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf +image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f audio caf=x-caf mp3=mpeg m4a=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr diff --git a/docs/chungus.conf b/docs/chungus.conf index 7ac7ba9a..a8e34516 100644 --- a/docs/chungus.conf +++ b/docs/chungus.conf @@ -1083,7 +1083,7 @@ th-x3: n # default # image decoders, in order of preference - th-dec: vips,pil,ff # default + th-dec: vips,pil,raw,ff # default # disable jpg output th-no-jpg @@ -1115,6 +1115,9 @@ # image formats to decode using pyvips th-r-vips: a,very,long,list,of,file,extensions # hint + # image formats to decode using rawpy + th-r-raw: a,very,long,list,of,file,extensions # hint + # image formats to decode using ffmpeg th-r-ffi: a,very,long,list,of,file,extensions # hint From 6ccc9224f308a8a171478c5645dc065273012100 Mon Sep 17 00:00:00 2001 From: Kamalei Zestri <38802353+KamaleiZestri@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:21:45 -0500 Subject: [PATCH 020/154] make rpm --- contrib/package/rpm/copyparty.spec | 62 ++++++++++++++++++++++++++++++ scripts/make-rpm.sh | 61 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 contrib/package/rpm/copyparty.spec create mode 100755 scripts/make-rpm.sh diff --git a/contrib/package/rpm/copyparty.spec b/contrib/package/rpm/copyparty.spec new file mode 100644 index 00000000..6d3ebb3f --- /dev/null +++ b/contrib/package/rpm/copyparty.spec @@ -0,0 +1,62 @@ +Name: copyparty +Version: $pkgver +Release: $pkgrel +License: MIT +Group: Utilities +URL: https://github.com/9001/copyparty +Source0: copyparty-$pkgver.tar.gz +Summary: File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ +BuildArch: noarch +BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make +Requires: python3, (python3-jinja2 or python-jinja2), lsof +Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips +Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-impacket + +%description +Portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps + +See release at https://github.com/9001/copyparty/releases + +%global debug_package %{nil} + +%generate_buildrequires +%pyproject_buildrequires + +%prep +%setup -q + +%build +cd "copyparty/web" +make +cd - +%pyproject_wheel + +%install +mkdir -p %{buildroot}%{_bindir} +mkdir -p %{buildroot}%{_libdir}/systemd/{system,user} +mkdir -p %{buildroot}/etc/%{name} +mkdir -p %{buildroot}/var/lib/%{name}-jail +mkdir -p %{buildroot}%{_datadir}/licenses/%{name} + +%pyproject_install +%pyproject_save_files copyparty + +install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh +install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf +install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service +install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service +install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service +install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md +install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE + +%files -n copyparty -f %{pyproject_files} +%license LICENSE +%{_bindir}/copyparty +%{_bindir}/partyfuse +%{_bindir}/u2c +%{_bindir}/prisonpary.sh +/etc/%{name}/%{name}.conf +%{_libdir}/systemd/system/%{name}@.service +%{_libdir}/systemd/user/%{name}.service +%{_libdir}/systemd/system/prisonparty@.service +/var/lib/%{name}-jail/README.md diff --git a/scripts/make-rpm.sh b/scripts/make-rpm.sh new file mode 100755 index 00000000..441ade5a --- /dev/null +++ b/scripts/make-rpm.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +#--localbuild to build webdeps and tar locally; otherwise just download prebuilt +#--pm change packagemanager; otherwise default to dnf + +while [ ! -z "$1" ]; do + case $1 in + local-build) local_build=1 ; ;; + pm) shift;packagemanager="$1"; ;; + esac + shift +done + +[ -e copyparty/__main__.py ] || cd .. +[ -e copyparty/__main__.py ] || +{ + echo "run me from within the project root folder" + echo + exit 1 +} + + +packagemanager=${packagemanager:-dnf} +ver=$(awk '/^VERSION/{gsub(/[^0-9]/," ");printf "%d.%d.%d\n",$1,$2,$3}' copyparty/__version__.py) +releasedir="dist/temp_copyparty_$ver" +sourcepkg="copyparty-$ver.tar.gz" + +#make temporary directory to build rpm in +mkdir -p $releasedir/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} +trap "rm -rf $releasedir/" EXIT + +# make/get tarball +if [ $local_build ]; then + if [ ! -f "copyparty/web/deps/mini-fa.woff" ]; then + sudo $packagemanager update + sudo $packagemanager install podman-docker docker + make -C deps-docker + fi + if [ ! -f "dist/$sourcepkg" ]; then + ./$cppdir/scripts/make-tgz-release.sh "$ver" + fi +else + if [ ! -f "dist/$sourcepkg" ]; then + curl -OL https://github.com/9001/copyparty/releases/download/v$ver/$sourcepkg --output-dir dist + fi +fi + +cp dist/$sourcepkg "$releasedir/SOURCES/$sourcepkg" + +cp "contrib/package/rpm/copyparty.spec" "$releasedir/SPECS/" +sed -i "s/\$pkgver/$ver/g" "$releasedir/SPECS/copyparty.spec" +sed -i "s/\$pkgrel/1/g" "$releasedir/SPECS/copyparty.spec" + +sudo $packagemanager update +sudo $packagemanager install rpmdevtools python-wheel python-setuptools pyproject-rpm-macros +cd "$releasedir/" +rpmbuild --define "_topdir `pwd`" -bb SPECS/copyparty.spec +cd - + +rpm="copyparty-$ver-1.noarch.rpm" +mv "$releasedir/RPMS/noarch/$rpm" dist/$rpm From 88243ac8d6e4b98e9e10f52d4733c3f7eca4f614 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 11 Aug 2025 18:35:49 +0000 Subject: [PATCH 021/154] make-rpm: small tweaks; * fail fast on error * ensure all deps Signed-off-by: ed --- scripts/make-rpm.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/make-rpm.sh b/scripts/make-rpm.sh index 441ade5a..5dd6573c 100755 --- a/scripts/make-rpm.sh +++ b/scripts/make-rpm.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e #--localbuild to build webdeps and tar locally; otherwise just download prebuilt #--pm change packagemanager; otherwise default to dnf @@ -27,7 +28,7 @@ sourcepkg="copyparty-$ver.tar.gz" #make temporary directory to build rpm in mkdir -p $releasedir/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} -trap "rm -rf $releasedir/" EXIT +trap "rm -rf $releasedir" EXIT # make/get tarball if [ $local_build ]; then @@ -37,6 +38,7 @@ if [ $local_build ]; then make -C deps-docker fi if [ ! -f "dist/$sourcepkg" ]; then + ./$cppdir/scripts/make-sfx.sh gz fast # pulls some build-deps + good smoketest ./$cppdir/scripts/make-tgz-release.sh "$ver" fi else @@ -52,7 +54,10 @@ sed -i "s/\$pkgver/$ver/g" "$releasedir/SPECS/copyparty.spec" sed -i "s/\$pkgrel/1/g" "$releasedir/SPECS/copyparty.spec" sudo $packagemanager update -sudo $packagemanager install rpmdevtools python-wheel python-setuptools pyproject-rpm-macros +sudo $packagemanager install \ + rpmdevtools python-devel pyproject-rpm-macros \ + python-wheel python-setuptools python-jinja2 \ + make pigz cd "$releasedir/" rpmbuild --define "_topdir `pwd`" -bb SPECS/copyparty.spec cd - From 1ebe06f51efaf3e77974a48b16f907992c2aaabf Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 11 Aug 2025 20:49:09 +0000 Subject: [PATCH 022/154] sticky qr-code; #533 --- copyparty/__main__.py | 7 ++++--- copyparty/svchub.py | 40 +++++++++++++++++++++++++++++++++++++++- copyparty/tcpsrv.py | 6 ++++++ tests/util.py | 2 +- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 946b3d0a..e1fcce2f 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1019,14 +1019,15 @@ def add_general(ap, nc, srvname): def add_qr(ap, tty): ap2 = ap.add_argument_group("qr options") - ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup") - ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup") + ap2.add_argument("--qr", action="store_true", help="show QR-code on startup") + ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://") ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]") ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead") - ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable") + ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable") ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)") ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)") ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") + ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr") def add_fs(ap): diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 5e0ab886..a6f87a85 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -2,6 +2,7 @@ from __future__ import print_function, unicode_literals import argparse +import atexit import errno import logging import os @@ -73,6 +74,7 @@ from .util import ( pybin, start_log_thrs, start_stackmon, + termsize, ub64enc, ) @@ -775,6 +777,39 @@ class SvcHub(object): def sigterm(self) -> None: self.signal_handler(signal.SIGTERM, None) + def sticky_qr(self) -> None: + tw, th = termsize() + zs1, qr = self.tcpsrv.qr.split("\n", 1) + url, colr = zs1.split(" ", 1) + nl = len(qr.split("\n")) # numlines + lp = 3 if nl * 2 + 4 < tw else 0 # leftpad + lp0 = lp + if self.args.qr_pin == 2: + url = "" + else: + while lp and (nl + lp) * 2 + len(url) + 1 > tw: + lp -= 1 + if (nl + lp) * 2 + len(url) + 1 > tw: + qr = url + "\n" + qr + url = "" + nl += 1 + lp = lp0 + sh = 1 + th - nl + if lp: + zs = " " * lp + qr = zs + qr.replace("\n", "\n" + zs) + if url: + url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url) + qr = colr + qr + + def unlock(): + print("\033[s\033[r\033[u", file=sys.stderr) + + atexit.register(unlock) + t = "%s\033[%dA" % ("\n" * nl, nl) + t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url) + self.pr(t, file=sys.stderr) + def cb_httpsrv_up(self) -> None: self.httpsrv_up += 1 if self.httpsrv_up != self.broker.num_workers: @@ -787,7 +822,10 @@ class SvcHub(object): break if self.tcpsrv.qr: - self.log("qr-code", self.tcpsrv.qr) + if self.args.qr_pin: + self.sticky_qr() + else: + self.log("qr-code", self.tcpsrv.qr) else: self.log("root", "workers OK\n") diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index 6c61fe61..55ea3a56 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -614,6 +614,10 @@ class TcpSrv(object): fg = self.args.qr_fg bg = self.args.qr_bg + nocolor = fg == -1 + if nocolor: + fg = 0 + pad = self.args.qrp zoom = self.args.qrz qrc = QrCode.encode_binary(btxt) @@ -641,6 +645,8 @@ class TcpSrv(object): qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m" + if nocolor: + cc = " \033[0m" t = cc + "\n{2}\033[999G\033[0m\033[J" t = t.format(fg, bg, qr) if ANYWIN: diff --git a/tests/util.py b/tests/util.py index d3884ca9..f02f81ba 100644 --- a/tests/util.py +++ b/tests/util.py @@ -161,7 +161,7 @@ class Cfg(Namespace): ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" ka.update(**{k: 9 for k in ex.split()}) - ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" + ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin 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 chmod_f chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" From a407eb92692cf09f27892cc0921a14db78abc403 Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 12 Aug 2025 12:31:23 -0300 Subject: [PATCH 023/154] warn if failed to import pyvips pyvips uses `ffi.callback()`, which fails on on some systems --- copyparty/th_srv.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index b37591c6..945c6d56 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -113,14 +113,17 @@ except: try: if os.environ.get("PRTY_NO_VIPS"): - raise Exception() + raise ImportError() HAVE_VIPS = True import pyvips logging.getLogger("pyvips").setLevel(logging.WARNING) -except: +except Exception as e: HAVE_VIPS = False + if not isinstance(e, ImportError): + logging.warning("libvips found, but failed to load: " + str(e)) + try: if os.environ.get("PRTY_NO_RAW"): From d8662aeb0ecb0409d1807e466fc5818303437e53 Mon Sep 17 00:00:00 2001 From: varphi <160203809+varphi-online@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:01:41 -0700 Subject: [PATCH 024/154] add win95 light and dark themes (#581) --- copyparty/__main__.py | 2 +- copyparty/web/browser.css | 787 ++++++++++++++++++++++++++++++++++++++ copyparty/web/browser.js | 2 +- 3 files changed, 789 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index e1fcce2f..97392c87 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1570,7 +1570,7 @@ def add_ui(ap, retry): ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)") - ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed") + ap2.add_argument("--themes", metavar="NUM", type=int, default=10, help="number of themes installed") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)") ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)") diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 298de0ea..f2dc8cde 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -3272,3 +3272,790 @@ html.d #treepar { transition: background-color .3s ease, color .3s ease; } } +html.ey { + --negative-space: 0em; /* Use this to change the global spacing of the 95 theme */ + --font-main: consolas; + --font-serif: consolas; + --font-mono: consolas; + --w: #fff; + --w2: #dfdfdf; + --w3: grey; + --fg: #000; + --fg-max: #0000ff; + --fg-weak: #0000ff; + --bg: #c6c3c6; + --bg-d3: #ff0; + --bg-d2: var(--w3); + --bg-d1: var(--bg); + --bg-u2: var(--bg); + --bg-u3: var(--bg); + --bg-u5: var(--shadow-color-2); + --tab-alt: #00f; + --g-fsel-bg: #00f; + --g-sel-bg: #00f; + --g-fsel-b1: #fff; + --row-alt: var(--w); + --scroll: var(--silver); + --f-sel-sh: transparent; + --a: #000; + --a-b: #fff; + --a-hil: #fff; + --a-h-bg: var(--bg); + --a-dark: var(--a); + --a-gray: var(--fg-weak); + --btn-fg: var(--fg); + --btn-bg: var(--bg); + --btn-h-fg: var(--fg); + --btn-h-bg: var(--bg); + --btn-1-fg: var(--fg); + --btn-1-bg: var(--bg); + --btn-1h-bg: var(--bg-d3); + --txt-sh: a; + --txt-bg: var(--white); + --u2-b1-bg: var(--w2); + --u2-b2-bg: var(--w2); + --u2-txt-bg: var(--w2); + --u2-tab-bg: a; + --u2-tab-1-bg: var(--w2); + --sort-1: var(--fg-weak); + --tree-bg: var(--w); + --g-b1: a; + --g-b2: a; + --g-f-bg: var(--w2); + --f-sh1: 0.1; + --f-sh2: 0.02; + --f-sh3: 0.1; + --f-h-b1: a; + --srv-1: var(--w); + --srv-3: var(--a); + --mp-sh: a; + --black: #000; + --white: #fff; + --grey: grey; + --silver: silver; + --transparent: transparent; + --shadow-color-1: #0a0a0a; + --shadow-color-2: #808080; + --border-dashed-black: 1px dashed var(--black); + --radius: 0; + --focus-outline: 1px dashed var(--black); + --hover-outline: 1px dotted var(--black); + --fm-off: var(--w3); + --ttlbar: linear-gradient(90deg, navy, #1084d0); + --inset-bg: var(--white); + --scroll-bkg: var(--white); + + /*All sides*/ + --shadow-outset: inset -1px -1px var(--shadow-color-1), + inset 1px 1px var(--white), inset -2px -2px var(--grey), + inset 2px 2px var(--w2); + + --shadow-inset: inset -1px -1px var(--white), + inset 1px 1px var(--shadow-color-1), inset -2px -2px var(--w2), + inset 2px 2px var(--shadow-color-2); + + --shadow-input: inset -1px -1px var(--white), inset 1px 1px var(--grey), + inset -2px -2px var(--w2), inset 2px 2px var(--shadow-color-1); + + /*Indiv sides*/ + --shadow-outset-bottom: inset 0 -1px var(--shadow-color-1), + inset 0 -2px var(--grey); + --shadow-outset-right: inset -1px 0 var(--shadow-color-1), + inset -2px 0 var(--grey); + --shadow-outset-left: inset 1px 0 var(--white), inset 2px 0 var(--w2); + --shadow-outset-top: inset 0 1px var(--white), inset 0 2px var(--w2); + + --shadow-inset-bottom: inset 0 -1px var(--white), inset 0 -2px var(--w2); + --shadow-inset-right: inset -1px 0 var(--white), inset -2px 0 var(--w2); + --shadow-inset-left: inset 1px 0 var(--shadow-color-1), + inset 2px 0 var(--shadow-color-2); + --shadow-inset-top: inset 0 1px var(--shadow-color-1), + inset 0 2px var(--shadow-color-2); +} +html.ez { + --negative-space: 0em; /* Use this to change the global spacing of your theme :) */ + --font-main: consolas; + --font-serif: consolas; + --font-mono: consolas; + --w: #fff; + --w2: var(--inset-bg); + --w3: grey; + --fg: #cfcfcf; + --fg-max: #47b8ff; + --fg-weak: #47b8ff; + --bg: #383838; + --bg-d3: #600000; + --bg-d2: var(--shadow-color-1); + --bg-d1: var(--bg); + --u2-tab-1-fg: #ff0; + --bg-u2: var(--bg); + --bg-u3: var(--bg); + --bg-u5: var(--shadow-color-2); + --tab-alt: #47b8ff; + --g-fsel-bg: #0000b7; + --g-sel-bg: #00f; + --g-fsel-b1: #fff; + --row-alt: #555555; + --scroll: #555555; + --f-sel-sh: transparent; + --a: var(--fg); + --a-b: var(--fg); + --a-hil: var(--fg); + --btn-1h-bg: var(--bg-d3); + --a-h-bg: var(--bg); + --a-dark: var(--a); + --a-gray: var(--fg-weak); + --btn-fg: var(--white); + --btn-bg: var(--bg); + --btn-h-fg: var(--white); + --btn-h-bg: var(--bg); + --btn-1-fg: var(--white); + --btn-1-bg: var(--bg); + --txt-sh: a; + --u2-b1-bg: var(--w2); + --u2-b2-bg: var(--w2); + --u2-txt-bg: var(--w2); + --u2-tab-bg: a; + --u2-tab-1-bg: var(--w2); + --sort-1: var(--fg-weak); + --g-b1: a; + --g-b2: a; + --g-f-bg: var(--w2); + --f-sh1: 0.1; + --f-sh2: 0.02; + --f-sh3: 0.1; + --f-h-b1: a; + --srv-1: var(--w); + --srv-3: var(--a); + --mp-sh: a; + --black: #000; + --white: #fff; + --grey: grey; + --silver: #858585; + --transparent: transparent; + --shadow-color-1: #101010; + --shadow-color-2: #1f1f1f; + --border-dashed-black: 1px dashed var(--shadow-color-1); + --radius: 0; + --focus-outline: 1px dashed var(--white); + --hover-outline: 1px dotted var(--white); + --fm-off: var(--w3); + --ttlbar: linear-gradient(90deg, var(--shadow-color-1) 20%, #888888); + --inset-bg: #3f3f3f; + --tree-bg: var(--inset-bg); + --txt-bg: var(--inset-bg); + --scroll-bkg: var(--black); + + /*All sides*/ + --shadow-outset: inset -1px -1px var(--shadow-color-1), inset 1px 1px #878787, + inset -2px -2px var(--shadow-color-2), inset 2px 2px #575757; + + --shadow-inset: inset -1px -1px #878787, inset 1px 1px var(--shadow-color-1), + inset -2px -2px #575757, inset 2px 2px var(--shadow-color-2); + + --shadow-input: inset -1px -1px var(--white), + inset 1px 1px var(--shadow-color-2), inset -2px -2px #575757, + inset 2px 2px var(--shadow-color-1); + + --shadow-outset-bottom: inset 0 -1px var(--shadow-color-1), + inset 0 -2px var(--shadow-color-2); + --shadow-outset-right: inset -1px 0 var(--shadow-color-1), + inset -2px 0 var(--shadow-color-2); + --shadow-outset-left: inset 1px 0 #878787, inset 2px 0 #575757; + --shadow-outset-top: inset 0 1px #878787, inset 0 2px #575757; + + --shadow-inset-bottom: inset 0 -1px #878787, inset 0 -2px #575757; + --shadow-inset-right: inset -1px 0 #878787, inset -2px 0 #575757; + --shadow-inset-left: inset 1px 0 var(--shadow-color-1), + inset 2px 0 var(--shadow-color-2); + --shadow-inset-top: inset 0 1px var(--shadow-color-1), + inset 0 2px var(--shadow-color-2); +} + +html.e { + text-shadow: none; +} + +html.e #files, +html.e #u2conf input[type="checkbox"]:hover + label, +html.e .tgl.btn.on:hover, +html.e body { + background: var(--bg); +} +html.e #pctl a, +html.e #repl, +html.e #u2conf a, +html.e #u2conf input[type="checkbox"] + label, +html.e #wfp a, +html.e .btn, +html.e .eq_step, +html.e input[type="submit"] { + box-shadow: var(--shadow-outset); + border-radius: var(--radius); + background: var(--bg); + border: 0; +} +a.s0r, +html.e #ghead a.s0, +html.e #u2conf input[type="checkbox"]:checked + label, +html.e .tgl.btn.on, +html.e input[type="submit"]:active { + box-shadow: var(--shadow-inset) !important; +} +html.e #ops a:hover, +html.e #pctl a:hover, +html.e #repl:hover, +html.e #u2conf a:hover, +html.e #u2conf input[type="checkbox"]:hover + label, +html.e #wfp a:hover, +html.e .btn:hover, +html.e .eq_step:hover, +html.e input[type="submit"]:hover { + outline: var(--hover-outline); + outline-offset: -4px; +} +html.e .ntree a:hover, +html.e :focus, +html.e :focus + label, +html.e a:active, +html.e tr:focus, +input[type="text"]:focus { + outline: var(--focus-outline) !important; +} +html.e tr:focus { + box-shadow: none; +} +html.e #pctl a:focus, +html.e #repl:hover, +html.e #u2conf input[type="checkbox"]:focus + label, +html.e #wfp a:focus, +html.e .btn:focus, +html.e .eq_step:focus { + border: 0 !important; + outline: var(--focus-outline) !important; + outline-offset: 2px; + box-shadow: var(--shadow-outset) !important; +} +html.e #files tbody, +html.e #u2cards a.act { + box-shadow: var(--shadow-inset); +} +html.e #files { + border: 2px groove var(--transparent); + box-sizing: border-box; + width: 100%; + padding: 0.3em; + top: 0; + border: 0; +} +html.e #files tbody tr td, +html.e #files thead th { + border-radius: var(--radius); +} +#files td { + background: var(--w2); +} +html.e #files tr { + background-color: var(--black); +} +html.e #srv_info span, +html.e label { + color: var(--btn-fg) !important; +} +html.e #acc_info { + background: var(--transparent); + color: var(--white); + height: 2em; + left: 1em; + width: fit-content; +} +html.e #acc_info, +html.e #ops, +html.e #srv_info { + display: flex; + align-items: center; +} +html.e #flogout:before { + padding-left: 0.2em; + padding-right: 0.4em; + content: " | "; +} +html.e #blogout { + color: var(--w); + box-shadow: none; + background: transparent; +} +html.e .opwide > div { + border-left: 1px solid var(--fg); +} +html.e #srv_info { + background: var(--transparent); + color: var(--white); + height: fit-content; + top: 3.2em; + left: 1em; + gap: 0.2em; +} +html.e #u2cards a.act { + padding: 0.2em 1em; +} +html.e #u2btn { + border: var(--border-dashed-black); + border-radius: var(--border-radius); + transform: translateY(30%); +} +html.e #ops, +html.e #ops a { + border-radius: var(--radius); +} +@media only screen and (max-width: 600px) { + html.e #acc_info { + background: var(--transparent); + color: var(--white); + height: fit-content; + align-items: center; + top: 3.2em; + right: 1em; + left: auto; + display: flex; + gap: 0.2em; + } + html.e #u2btn { + transform: none; + } +} +html.e #ops { + background: var(--ttlbar); + /*HC*/ + box-shadow: inset 0-1px grey, inset 0-2px var(--shadow-color-1); + height: 2em; + gap: 0.6em; + padding: 0.2em; + flex-direction: row-reverse; + margin-bottom: 1.2em; +} +html.e #srch_form, +html.e .opbox { + padding-bottom: 1em; + padding-top: 1em; + max-width: 100vw; +} +html.e #ghead, +html.e #ops a { + align-items: center; + display: flex; +} +html.e #ops a { + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); + height: 1.4em; + padding: 0; + box-shadow: var(--shadow-outset); + background: var(--bg); + aspect-ratio: 1/1; + justify-content: center; + font-size: 1.25em; + z-index: 4; +} +html.e #blogout:focus, +html.e #ops a:focus { + outline: 1px dashed var(--w) !important; +} + +html.e #blogout:hover { + text-decoration: underline; +} + +html.e #ops > a:not(:first-child).act { + height: 1.4em; + width: 1.4em; + padding-bottom: 0.3em; + margin-top: 0.3em; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + box-shadow: var(--shadow-inset-left), var(--shadow-inset-top), + var(--shadow-inset-right); + z-index: 6; +} +html.e #ops a.act { + box-shadow: var(--shadow-inset); + border-bottom: 0; +} +html.e a:active { + border: 0; +} +html.e :focus, +html.e :focus + label { + border: 0 !important; + outline-offset: 1px; + border-radius: var(--radius) !important; + box-shadow: inherit; +} +html.e #opa_x { + text-shadow: 0 0 0 var(--transparent) !important; + color: var(--bg) !important; + display: flex; +} +html.e #opa_x:before { + content: "⨯"; + color: var(--fg) !important; + margin-top: -0.1em; + font-size: 1.75em; + position: absolute; +} +html.e .opbox { + margin: -1.2em 0 0; + box-shadow: var(--shadow-inset-bottom), var(--shadow-inset-left), + var(--shadow-inset-right); + border-radius: var(--radius); + z-index: 5; + background: var(--bg); +} +html.e #srch_form { + margin: 0; + border-radius: var(--radius); +} +html.e #op_unpost { + max-width: 100vw; + margin: 0; +} +html.e label:focus { + box-shadow: 0 0; +} +html.e #tree { + box-shadow: none; + padding-right: 5px; +} +html.e #tt { + background: var(--w2); +} +html.e .mdo a { + background: 0 0; + text-decoration: underline; +} +html.e .mdo code, +html.e .mdo pre { + color: var(--white); + background: var(--w2); + border: 0; +} +html.e .mdo h1, +html.e .mdo h2 { + background: 0 0; + border-color: var(--w2); +} +html.e #tt, +html.e .mdo ol ol, +html.e .mdo ol ul, +html.e .mdo ul ol, +html.e .mdo ul ul { + border-color: var(--w2); +} +html.e .mdo li > em, +html.e .mdo p > em, +html.e .mdo td > em { + color: #fd0; +} +html.e input.txtbox, +html.e input[type="text"], +html.e select { + background-color: var(--txt-bg); + box-shadow: var(--shadow-input) !important; + box-sizing: border-box; + padding: 3px 4px; + border-radius: var(--radius); + border: 0; +} +html.e #gfiles { + box-shadow: var(--shadow-outset); + background: var(--bg); + padding: 0.4em; + display: flex; + flex-direction: column; + gap: 0.3em; +} +html.e #ggrid { + background-color: var(--inset-bg); + box-shadow: var(--shadow-input); + padding: 1.5em; + margin: 0; + overflow-x: scroll; +} +html.e #ghead { + margin: 0; + justify-content: flex-end; + gap: 0.4em; + padding: 0; + overflow: auto; + top: 0px; + border-radius: 0px; +} +html.e #ghead a { + margin: 0; + border-radius: var(--radius); +} +html.e ::-webkit-scrollbar, +html.e::-webkit-scrollbar { + width: 16px !important; + height: 16px !important; + background: var(--transparent) !important; +} +html.e ::-webkit-scrollbar-button, +html.e ::-webkit-scrollbar-thumb, +html.e::-webkit-scrollbar-button, +html.e::-webkit-scrollbar-thumb { + width: 16px !important; + height: 16px !important; + background: var(--scroll) !important; + /*HC*/ + box-shadow: var(--shadow-outset); + border: 1px solid !important; + border-color: var(--silver) var(--black) var(--black) var(--silver) !important; +} +html.e ::-webkit-scrollbar-track, +html.e::-webkit-scrollbar-track { + image-rendering: optimize-contrast !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAyIDIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CjxtZXRhZGF0YT5NYWRlIHdpdGggUGl4ZWxzIHRvIFN2ZyBodHRwczovL2NvZGVwZW4uaW8vc2hzaGF3L3Blbi9YYnh2Tmo8L21ldGFkYXRhPgo8cGF0aCBzdHJva2U9IiNjMGMwYzAiIGQ9Ik0wIDBoMU0xIDFoMSIgLz4KPC9zdmc+) !important; + background-position: 0 0 !important; + background-repeat: repeat !important; + background-size: 2px !important; + background: var(--scroll-bkg); +} +#tree::-webkit-scrollbar, +#tree::-webkit-scrollbar-track { + background: var(--scroll-bkg); +} +html.e ::-webkit-scrollbar-button, +html.e::-webkit-scrollbar-button { + background-repeat: no-repeat !important; + background-size: 16px !important; +} +html.e ::-webkit-scrollbar-button:single-button:vertical:decrement, +html.e::-webkit-scrollbar-button:single-button:vertical:decrement { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTcgNWgxTTYgNmgzTTUgN2g1TTQgOGg3IiAvPgo8L3N2Zz4=) !important; +} +html.e ::-webkit-scrollbar-button:single-button:vertical:increment, +html.e::-webkit-scrollbar-button:single-button:vertical:increment { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNWg3TTUgNmg1TTYgN2gzTTcgOGgxIiAvPgo8L3N2Zz4=) !important; +} +html.e ::-webkit-scrollbar-button:single-button:horizontal:decrement, +html.e::-webkit-scrollbar-button:single-button:horizontal:decrement { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTggM2gxTTcgNGgyTTYgNWgzTTUgNmg0TTYgN2gzTTcgOGgyTTggOWgxIiAvPgo8L3N2Zz4=) !important; +} +html.e ::-webkit-scrollbar-button:single-button:horizontal:increment, +html.e::-webkit-scrollbar-button:single-button:horizontal:increment { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTYgM2gxTTYgNGgyTTYgNWgzTTYgNmg0TTYgN2gzTTYgOGgyTTYgOWgxIiAvPgo8L3N2Zz4=) !important; +} +html.e ::-webkit-scrollbar-corner, +html.e::-webkit-scrollbar-corner { + background: var(--silver) !important; +} +html, +html.e #tree { + scrollbar-color: inherit !important; +} +html.e #tree { + background: var(--bg); + padding-left: 0.4em; + padding-top: 0; + margin-left: var(--negative-space); +} +html.e.noscroll #tree { + /*HC*/ + box-shadow: 1px 1px var(--grey), 2px 2px var(--shadow-color-1), + var(--shadow-outset-bottom); +} +html.e #treeh { + background: var(--bg); + box-shadow: var(--shadow-outset-top), var(--shadow-outset-bottom); + width: calc(1.5em + var(--nav-sz) - var(--sbw)); + height: 2.4em; + border: none; + top: -2px; + display: flex; + align-items: center; + gap: 0.6em; +} +html.e #treeh .btn { + margin: 0px; + top: auto; +} +html.e #tree ul { + border-left: var(--border-dashed-black); + margin-left: 2.15em; +} +html.e .ntree a:first-child { + font-family: scp, monospace, monospace; + font-size: 1.2em; + line-height: 0; + background: var(--inset-bg); + aspect-ratio: 1/1; + text-align: center; + align-content: center; + border-radius: var(--radius) !important; + padding: 0.057em; + border: 1px solid var(--black); +} +html.e .ntree a:first-child:after { + content: "."; + position: absolute; + border-top: var(--border-dashed-black); + color: var(--transparent); + font-size: 0.9em; + margin-left: 0.13em; +} +html.e #treeul { + border: 0 !important; + position: static; + margin: 0 !important; + min-height: 100%; + height: max-content; +} +html.e .ntree a:last-of-type:before { + content: "📁"; + margin-left: 0.3em; +} +html.e .ntree { + padding-left: 1em !important; + padding-top: 0.3em !important; + background: var(--inset-bg); + box-shadow: var(--shadow-inset-left), var(--shadow-inset-bottom); +} +html.e #tree li { + margin-left: -0.5em; + border-top: 0; +} +html.e .ntree a:hover { + outline-offset: -2px; + color: var(--fg); + border-radius: var(--radius) !important; +} +html.e #treepar { + width: calc(-1em + var(--nav-sz) - var(--sbw)); + overflow: hidden; + left: -0.7em; + box-shadow: var(--shadow-inset-left), var(--shadow-inset-top); + border-left: 0 !important; + border-bottom: var(--border-dashed-black); + margin-left: calc(2.1em - (1em - var(--negative-space))) !important; +} +html.e #path, +html.e #widgeti, +html.e #wtoggle, +html.e #wtoggle a, +html.e #files, +html.e #files thead th, +html.e #ghead a, +html.e #tree { + box-shadow: var(--shadow-outset); +} +html.e.noscroll #treepar { + width: calc(var(--nav-sz) - 1em); +} +html.e #docul { + border-left: 0 !important; + margin-left: 0 !important; +} +html.e #wrap { + transform: translateX(calc((var(--negative-space) * 2) - 1.2em)); + padding-right: var(--negative-space); + position: relative; + margin-right: calc((var(--negative-space) * 2) - 1.2em); + margin-top: var(--negative-space); + margin-left: 1.2em; + /*overflow-x: auto; fix for OOB table when screen space is limited (mobile), but removes sticky header*/ +} +html.e input[type="radio"] { + accent-color: #232323; +} +html.e #path { + width: calc(100% - 0.4em); + display: flex; + align-items: center; + margin: 0; + padding: 0.2em; + overflow-x: auto; +} +html.e #path i { + border: 1px solid var(--w); + border-color: var(--w); + margin: 0; + border-width: 0.1em 0.1em 0 0; + height: 0.5em; + width: 0.5em; +}/* +html.e #hovertree:after { + color: red; + content: "BUGGY"; +html.ez #hovertree:after { + color: rgb(255 98 98); + content: "BUGGY"; +} +}*/ +html.e #widget { + box-shadow: 0 0; + border: 0 !important; +} +html.e #wtico, +html.e #zip1 { + box-shadow: 0 0 !important; +} +html.e #wtgrid { + top: -0.09em; +} +html.e #wfs, +html.e #wm3u, +html.e #wnp, +html.e #wzip { + border-width: 0 1px 0 0; +} +html.e #wfm.act + #wzip1 + #wzip, +html.e #wfm.act + #wzip1 + #wzip + #wnp { + border-left-width: 1px; +} +html.e #barpos { + /* border-radius: var(--radius); */ + box-shadow: var(--shadow-inset); +} +html.e #goh + span { + border-left: 0.1em solid var(--bg-u5); +} +html.e #wfp { + margin: var(--negative-space); + font-size: 0; + display: inline-block; +} +html.e #wfp a { + font-size: large; + display: inline-block; +} +html.e #repl { + font-size: large; + padding: 0.33em; + right: calc(var(--negative-space) * 0.89); + position: absolute; +} +html.e #epi { + text-align: center; + text-wrap-mode: nowrap; + margin: 0px; +} + +html.e #epi.logue:not(.mdo) { + padding: 0.8em; + box-shadow: var(--shadow-outset); +} + +html.e #epi.logue.mdo { + padding-left: 3px; +} + +html.e #doc { + box-shadow: var(--shadow-inset); + background: var(--inset-bg); + margin: 0.2em; + border-radius: var(--radius); +} + +html.e #detree { + padding: 0px; +} diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 9739e55a..9734090d 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -16746,7 +16746,7 @@ var settheme = (function () { var html = [], cb = ebi('themes'), itheme = ax.indexOf(theme[0]) * 2 + (light ? 1 : 0), - names = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand', 'hacker', 'hi-con']; + names = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand', 'hacker', 'hi-con', 'win95 dark', 'win95']; for (var a = 0; a < themes; a++) html.push(''.format(a, names[a] || 'custom')); From 69d9878acd06b8a9423512580e843dc73247a2f3 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 12 Aug 2025 19:03:34 +0000 Subject: [PATCH 025/154] use the good name --- 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 9734090d..6c79f3ef 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -16746,7 +16746,7 @@ var settheme = (function () { var html = [], cb = ebi('themes'), itheme = ax.indexOf(theme[0]) * 2 + (light ? 1 : 0), - names = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand', 'hacker', 'hi-con', 'win95 dark', 'win95']; + names = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand', 'hacker', 'hi-con', 'phi95 dark', 'phi95']; for (var a = 0; a < themes; a++) html.push(''.format(a, names[a] || 'custom')); From c32a672a6891961a7c61d2c6d38bfaa99716397e Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 12 Aug 2025 19:34:44 +0000 Subject: [PATCH 026/154] fix transcoding tooltips; closes #580 --- copyparty/web/browser.js | 93 +++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 6c79f3ef..a0a1a21c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -300,9 +300,9 @@ var Ls = { "mt_mloop": "loop the open folder\">🔁 loop", "mt_mnext": "load the next folder and continue\">📂 next", "mt_mstop": "stop playback\">⏸ stop", - "mt_cflac": "convert flac / wav to opus\">flac", - "mt_caac": "convert aac / m4a to opus\">aac", - "mt_coth": "convert all others (not mp3) to opus\">oth", + "mt_cflac": "convert flac / wav to {0}\">flac", + "mt_caac": "convert aac / m4a to {0}\">aac", + "mt_coth": "convert all others (not mp3) to {0}\">oth", "mt_c2opus": "best choice for desktops, laptops, android\">opus", "mt_c2owa": "opus-weba, for iOS 17.5 and newer\">owa", "mt_c2caf": "opus-caf, for iOS 11 through 17\">caf", @@ -930,9 +930,9 @@ var Ls = { "mt_mloop": "repeter hele mappen\">🔁 gjenta", "mt_mnext": "hopp til neste mappe og fortsett\">📂 neste", "mt_mstop": "stopp avspilling\">⏸ stopp", - "mt_cflac": "konverter flac / wav-filer til opus\">flac", - "mt_caac": "konverter aac / m4a-filer til to opus\">aac", - "mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre", + "mt_cflac": "konverter flac / wav-filer til {0}\">flac", + "mt_caac": "konverter aac / m4a-filer til to {0}\">aac", + "mt_coth": "konverter alt annet (men ikke mp3) til {0}\">andre", "mt_c2opus": "det beste valget for alle PCer og Android\">opus", "mt_c2owa": "opus-weba, for iOS 17.5 og nyere\">owa", "mt_c2caf": "opus-caf, for iOS 11 tilogmed 17\">caf", @@ -1559,9 +1559,9 @@ var Ls = { "mt_mloop": "循环打开的文件夹\">🔁 循环", "mt_mnext": "加载下一个文件夹并继续\">📂 下一首", "mt_mstop": "停止播放\">⏸ 停止", //m - "mt_cflac": "将 flac / wav 转换为 opus\">flac", - "mt_caac": "将 aac / m4a 转换为 opus\">aac", - "mt_coth": "将所有其他(不是 mp3)转换为 opus\">oth", + "mt_cflac": "将 flac / wav 转换为 {0}\">flac", + "mt_caac": "将 aac / m4a 转换为 {0}\">aac", + "mt_coth": "将所有其他(不是 mp3)转换为 {0}\">oth", "mt_c2opus": "适合桌面电脑、笔记本电脑和安卓设备的最佳选择\">opus", //m "mt_c2owa": "opus-weba(适用于 iOS 17.5 及更新版本)\">owa", //m "mt_c2caf": "opus-caf(适用于 iOS 11 到 iOS 17)\">caf", //m @@ -2192,9 +2192,9 @@ var Ls = { "mt_mloop": "opakovat otevřenou složku\">🔁 loop", "mt_mnext": "načíst další složku a pokračovat\">📂 next", "mt_mstop": "zastavit přehrávání\">⏸ stop", - "mt_cflac": "převést flac / wav na opus\">flac", - "mt_caac": "převést aac / m4a na opus\">aac", - "mt_coth": "převést všechny ostatní (ne mp3) na opus\">oth", + "mt_cflac": "převést flac / wav na {0}\">flac", + "mt_caac": "převést aac / m4a na {0}\">aac", + "mt_coth": "převést všechny ostatní (ne mp3) na {0}\">oth", "mt_c2opus": "nejlepší volba pro desktopy, laptopy, android\">opus", "mt_c2owa": "opus-weba, pro iOS 17.5 a novější\">owa", "mt_c2caf": "opus-caf, pro iOS 11 až 17\">caf", @@ -2821,9 +2821,9 @@ var Ls = { "mt_mloop": "offenen Ordner wiederholen\">🔁 Schleife", "mt_mnext": "nächsten Ordner laden und fortfahren\">📂 nächster", "mt_mstop": "Wiedergabe beenden\">⏸ Stop", - "mt_cflac": "FLAC / WAV zu OPUS konvertierebn\">flac", - "mt_caac": "AAC / M4A zu OPUS konvertieren\">aac", - "mt_coth": "Convertiere alle Dateien (die nicht MP3 sind) zu OPUS\">oth", + "mt_cflac": "FLAC / WAV zu {0} konvertierebn\">flac", + "mt_caac": "AAC / M4A zu {0} konvertieren\">aac", + "mt_coth": "Convertiere alle Dateien (die nicht MP3 sind) zu {0}\">oth", "mt_c2opus": "Beste Wahl für Desktops, Laptops, Android\">opus", "mt_c2owa": "opus-weba, für iOS 17.5 und neuer\">owa", "mt_c2caf": "opus-caf, für iOS 11 bis 17\">caf", @@ -3450,9 +3450,9 @@ var Ls = { "mt_mloop": "toista avoinna olevaa hakemistoa loputtomasti\">🔁 alkuun", "mt_mnext": "lataa seuraava hakemisto ja jatka\">📂 seuraava", "mt_mstop": "pysäytä toisto\">⏸ pysäytä", - "mt_cflac": "muunna flac / wav opus-muotoon\">flac", - "mt_caac": "muunna aac / m4a opus-muotoon\">aac", - "mt_coth": "muunna kaikki muut paitsi mp3 opus-muotoon\">muut", + "mt_cflac": "muunna flac / wav {0}-muotoon\">flac", + "mt_caac": "muunna aac / m4a {0}-muotoon\">aac", + "mt_coth": "muunna kaikki muut paitsi mp3 {0}-muotoon\">muut", "mt_c2opus": "paras valinta pöytäkoneille, kannettaville, androidille\">opus", "mt_c2owa": "opus-weba, iOS 17.5:lle ja uudemmille\">owa", "mt_c2caf": "opus-caf, iOS 11:lle - 17:lle\">caf", @@ -4079,9 +4079,9 @@ var Ls = { "mt_mloop": "lire en boucle le dossier ouvert\">🔁 loop", "mt_mnext": "charger le dossier suivant et continuer\">📂 next", "mt_mstop": "arrêter la lecture\">⏸ stop", - "mt_cflac": "convertir flac / wav en opus\">flac", - "mt_caac": "convertir aac / m4a en opus\">aac", - "mt_coth": "convertir tout les autres (pas mp3) en opus\">oth", + "mt_cflac": "convertir flac / wav en {0}\">flac", + "mt_caac": "convertir aac / m4a en {0}\">aac", + "mt_coth": "convertir tout les autres (pas mp3) en {0}\">oth", "mt_c2opus": "meilleur choix pour PC fixe, PC portable, android\">opus", "mt_c2owa": "opus-weba, pour iOS 17.5 et supérieur\">owa", "mt_c2caf": "opus-caf, pour iOS 11 à 17\">caf", @@ -4708,9 +4708,9 @@ var Ls = { "mt_mloop": "τυχαία αναπαραγωγή στον ανοικτό φάκελο\">🔁 τυχαία αναπαραγωγή", "mt_mnext": "φόρτωση επόμενου φακέλου και συνέχιση\">📂 επόμενο", "mt_mstop": "σταμάτησε την αναπαραγωγή\">⏸ σταμάτημα", - "mt_cflac": "μετατροπή flac / wav σε opus\">flac", - "mt_caac": "μετατροπή aac / m4a σε opus\">aac", - "mt_coth": "μετατροπή όλων των άλλων (εκτός των mp3) σε opus\">άλλο", + "mt_cflac": "μετατροπή flac / wav σε {0}\">flac", + "mt_caac": "μετατροπή aac / m4a σε {0}\">aac", + "mt_coth": "μετατροπή όλων των άλλων (εκτός των mp3) σε {0}\">άλλο", "mt_c2opus": "καλύτερη επιλογή για desktop, laptop, android\">opus", "mt_c2owa": "opus-weba, για iOS 17.5 και νεότερα\">owa", "mt_c2caf": "opus-caf, για iOS 11 έως 17\">caf", @@ -5337,9 +5337,9 @@ var Ls = { "mt_mloop": "loop della cartella aperta\">🔁 loop", "mt_mnext": "carica la prossima cartella e continua\">📂 succ", "mt_mstop": "ferma riproduzione\">⏸ stop", - "mt_cflac": "converti flac / wav in opus\">flac", - "mt_caac": "converti aac / m4a in opus\">aac", - "mt_coth": "converti tutti gli altri (non mp3) in opus\">oth", + "mt_cflac": "converti flac / wav in {0}\">flac", + "mt_caac": "converti aac / m4a in {0}\">aac", + "mt_coth": "converti tutti gli altri (non mp3) in {0}\">oth", "mt_c2opus": "scelta migliore per desktop, laptop, android\">opus", "mt_c2owa": "opus-weba, per iOS 17.5 e più recenti\">owa", "mt_c2caf": "opus-caf, per iOS 11 fino a 17\">caf", @@ -5966,9 +5966,9 @@ var Ls = { "mt_mloop": "De open map herhalen\">🔁 loop", "mt_mnext": "Laad de volgende map en ga verder\">📂 next", "mt_mstop": "Stoppen met afspelen\">⏸ stop", - "mt_cflac": "flac / wav omzetten naar opus\">flac", - "mt_caac": "aac / m4a omzetten naar opus\">aac", - "mt_coth": "Alle andere bestanden (geen mp3) converteren naar opus\">oth", + "mt_cflac": "flac / wav omzetten naar {0}\">flac", + "mt_caac": "aac / m4a omzetten naar {0}\">aac", + "mt_coth": "Alle andere bestanden (geen mp3) converteren naar {0}\">oth", "mt_c2opus": "Beste keuze voor computers, laptops, android\">opus", "mt_c2owa": "opus-weba, voor iOS 17.5 en nieuwer\">owa", "mt_c2caf": "opus-caf, voor iOS 11 tot en met iOS 17\">caf", @@ -6596,9 +6596,9 @@ var Ls = { "mt_mloop": "repetér heile mappa\">🔁 gjenta", "mt_mnext": "hopp åt neste mappe og fortsett\">📂 neste", "mt_mstop": "stopp avspeling\">⏸ stopp", - "mt_cflac": "konvertér flac / wav-filer åt opus\">flac", - "mt_caac": "konvertér aac / m4a-filer åt to opus\">aac", - "mt_coth": "konvertér alt anna (men ikkje mp3) åt opus\">andre", + "mt_cflac": "konvertér flac / wav-filer åt {0}\">flac", + "mt_caac": "konvertér aac / m4a-filer åt to {0}\">aac", + "mt_coth": "konvertér alt anna (men ikkje mp3) åt {0}\">andre", "mt_c2opus": "det beste valget for alle PCar og Android\">opus", "mt_c2owa": "opus-weba, for iOS 17.5 og nyare\">owa", "mt_c2caf": "opus-caf, for iOS 11 åt og med 17\">caf", @@ -7224,9 +7224,9 @@ var Ls = { "mt_mloop": "odtwarzaj utwory w folderze w pętli\">🔁 loop", "mt_mnext": "wczytaj następny folder i kontynuuj\">📂 next", "mt_mstop": "zatrzymaj odtwarzanie\">⏸ stop", - "mt_cflac": "przekonwertuj format flac / wav na opus\">flac", - "mt_caac": "przekonwertuj format aac / m4a na opus\">aac", - "mt_coth": "przekonwertuj wszystkie inne formaty (nie będące mp3) na opus\">oth", + "mt_cflac": "przekonwertuj format flac / wav na {0}\">flac", + "mt_caac": "przekonwertuj format aac / m4a na {0}\">aac", + "mt_coth": "przekonwertuj wszystkie inne formaty (nie będące mp3) na {0}\">oth", "mt_c2opus": "najlepszy wybór dla komputerów, laptopów i urządzeń z androidem\">opus", "mt_c2owa": "opus-weba, dla iOS 17.5 i nowszych\">owa", "mt_c2caf": "opus-caf, dla iOS 11 do 17\">caf", @@ -7851,9 +7851,9 @@ var Ls = { "mt_mloop": "повторять треки в папке\">🔁 цикл", "mt_mnext": "загрузить следующую папку и продолжить в ней\">📂 след.", "mt_mstop": "приостановить воспроизведение\">⏸ стоп", - "mt_cflac": "конвертировать flac / wav в opus\">flac", - "mt_caac": "конвертировать aac / m4a в opus\">aac", - "mt_coth": "конвертировать всё остальное (кроме mp3) в opus\">др.", + "mt_cflac": "конвертировать flac / wav в {0}\">flac", + "mt_caac": "конвертировать aac / m4a в {0}\">aac", + "mt_coth": "конвертировать всё остальное (кроме mp3) в {0}\">др.", "mt_c2opus": "лучший вариант для компьютеров и устройств на Android\">opus", "mt_c2owa": "opus-weba, для iOS 17.5 и выше\">owa", "mt_c2caf": "opus-caf, для iOS 11-17\">caf", @@ -8479,9 +8479,9 @@ var Ls = { "mt_mloop": 'repetir la carpeta actual">🔁 bucle', "mt_mnext": 'cargar la siguiente carpeta y continuar">📂 sig', "mt_mstop": 'detener reproducción">⏸ parar', - "mt_cflac": 'convertir flac / wav a opus">flac', - "mt_caac": 'convertir aac / m4a a opus">aac', - "mt_coth": 'convertir todos los demás (no mp3) a opus">oth', + "mt_cflac": 'convertir flac / wav a {0}">flac', + "mt_caac": 'convertir aac / m4a a {0}">aac', + "mt_coth": 'convertir todos los demás (no mp3) a {0}">oth', "mt_c2opus": 'la mejor opción para ordenadores, portátiles, android">opus', "mt_c2owa": 'opus-weba, para iOS 17.5 y superior">owa', "mt_c2caf": 'opus-caf, para iOS 11 a 17">caf', @@ -9108,9 +9108,9 @@ var Ls = { "mt_mloop": "зациклити відкриту папку\">🔁 loop", "mt_mnext": "завантажити наступну папку і продовжити\">📂 next", "mt_mstop": "зупинити відтворення\">⏸ stop", - "mt_cflac": "конвертувати flac / wav в opus\">flac", - "mt_caac": "конвертувати aac / m4a в opus\">aac", - "mt_coth": "конвертувати всі інші (не mp3) в opus\">oth", + "mt_cflac": "конвертувати flac / wav в {0}\">flac", + "mt_caac": "конвертувати aac / m4a в {0}\">aac", + "mt_coth": "конвертувати всі інші (не mp3) в {0}\">oth", "mt_c2opus": "найкращий вибір для робочих столів, ноутбуків, android\">opus", "mt_c2owa": "opus-weba, для iOS 17.5 і новіших\">owa", "mt_c2caf": "opus-caf, для iOS 11 до 17\">caf", @@ -10157,6 +10157,9 @@ var mpl = (function () { clmod(btns[a], 'on', fmts[a] == v) r.ac2 = v; + ebi('ac_flac').setAttribute('tt', L.mt_cflac.split('"')[0].format(v)); + ebi('ac_aac').setAttribute('tt', L.mt_caac.split('"')[0].format(v)); + ebi('ac_oth').setAttribute('tt', L.mt_coth.split('"')[0].format(v)); }; r.pp = function () { From c9fd608732914002b14e0961bf216f26cb46ca46 Mon Sep 17 00:00:00 2001 From: Danny Piper Date: Tue, 12 Aug 2025 20:41:16 +0100 Subject: [PATCH 027/154] feat: added cr3 to raw formats list Signed-off-by: Danny Piper --- copyparty/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 97392c87..ddfdd8a2 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1447,7 +1447,7 @@ def add_thumbnail(ap): # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") - ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy") + ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") From 715d374ee4d849ef4e7da2aec84f68f5dad87458 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 12 Aug 2025 21:46:42 +0000 Subject: [PATCH 028/154] button to abort copy/move; closes #572 --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 20 +++++++++++++++-- copyparty/svchub.py | 2 +- copyparty/up2k.py | 12 ++++++++-- copyparty/web/browser.css | 5 +++++ copyparty/web/browser.js | 46 +++++++++++++++++++++++++++++++++++---- tests/util.py | 2 +- 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index ddfdd8a2..86d8cdd5 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1319,6 +1319,7 @@ def add_optouts(ap): ap2.add_argument("--no-del", action="store_true", help="disable delete operations") ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations") ap2.add_argument("--no-cp", action="store_true", help="disable copy operations") + ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move") ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in ") 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") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 72bff45e..a7ff303d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1987,6 +1987,9 @@ class HttpCli(object): if "eshare" in self.uparam: return self.handle_eshare() + if "fs_abrt" in self.uparam: + return self.handle_fs_abrt() + if "application/octet-stream" in ctype: return self.handle_post_binary() @@ -5958,7 +5961,9 @@ class HttpCli(object): self.asrv.vfs.get(vdst, self.uname, False, True, False, True) wunlink(self.log, dabs, dvn.flags) - x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst) + x = self.conn.hsrv.broker.ask( + "up2k.handle_mv", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst + ) self.loud_reply(x.get(), status=201) return True @@ -5988,10 +5993,21 @@ class HttpCli(object): self.asrv.vfs.get(vdst, self.uname, False, True, False, True) wunlink(self.log, dabs, dvn.flags) - x = self.conn.hsrv.broker.ask("up2k.handle_cp", self.uname, self.ip, vsrc, vdst) + x = self.conn.hsrv.broker.ask( + "up2k.handle_cp", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst + ) self.loud_reply(x.get(), status=201) return True + def handle_fs_abrt(self): + if self.args.no_fs_abrt: + t = "aborting an ongoing copy/move is disabled in server config" + raise Pebkac(403, t) + + self.conn.hsrv.broker.say("up2k.handle_fs_abrt", self.uparam["fs_abrt"]) + self.loud_reply("aborting", status=200) + return True + def tx_ls(self, ls: dict[str, Any]) -> bool: dirs = ls["dirs"] files = ls["files"] diff --git a/copyparty/svchub.py b/copyparty/svchub.py index a6f87a85..950122d0 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -39,9 +39,9 @@ from .th_srv import ( HAVE_FFPROBE, HAVE_HEIF, HAVE_PIL, + HAVE_RAW, HAVE_VIPS, HAVE_WEBP, - HAVE_RAW, ThumbSrv, ) from .up2k import Up2k diff --git a/copyparty/up2k.py b/copyparty/up2k.py index b233cecb..993d5b82 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -144,6 +144,7 @@ class Up2k(object): self.salt = self.args.warksalt self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") + self.abrt_key = "" self.gid = 0 self.gt0 = 0 @@ -3988,6 +3989,9 @@ class Up2k(object): except: pass + def handle_fs_abrt(self, akey: str) -> None: + self.abrt_key = akey + def handle_rm( self, uname: str, @@ -4197,7 +4201,7 @@ class Up2k(object): return n_files, ok + ok2, ng + ng2 - def handle_cp(self, uname: str, ip: str, svp: str, dvp: str) -> str: + def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: if svp == dvp or dvp.startswith(svp + "/"): raise Pebkac(400, "cp: cannot copy parent into subfolder") @@ -4244,6 +4248,8 @@ class Up2k(object): dvpf = dvp + svpf[len(svp) :] self._cp_file(uname, ip, svpf, dvpf, curs) + if abrt and abrt == self.abrt_key: + raise Pebkac(400, "filecopy aborted by http-api") for v in curs: v.connection.commit() @@ -4411,7 +4417,7 @@ class Up2k(object): return "k" - def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str: + def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: if svp == dvp or dvp.startswith(svp + "/"): raise Pebkac(400, "mv: cannot move parent into subfolder") @@ -4466,6 +4472,8 @@ class Up2k(object): dvpf = dvp + svpf[len(svp) :] self._mv_file(uname, ip, svpf, dvpf, curs) + if abrt and abrt == self.abrt_key: + raise Pebkac(400, "filemove aborted by http-api") for v in curs: v.connection.commit() diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index f2dc8cde..00e42cae 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1930,6 +1930,11 @@ html.y #tree.nowrap .ntree a+a:hover { padding: 0; font-size: 1.5em; } +#fs_abrt { + margin-top: 1em; + text-shadow: 0; + box-shadow: 1px 1px 0 var(--bg-d3); +} #doc { overflow: visible; background: #fff; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index a0a1a21c..112e7da0 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -427,6 +427,7 @@ var Ls = { "fcp_ok": "copy OK", "fp_busy": "moving {0} items...\n\n{1}", "fcp_busy": "copying {0} items...\n\n{1}", + "fp_abrt": "aborting...", "fp_err": "move failed:\n", "fcp_err": "copy failed:\n", "fp_confirm": "move these {0} items here?", @@ -1057,6 +1058,7 @@ var Ls = { "fcp_ok": "kopiering OK", "fp_busy": "flytter {0} filer...\n\n{1}", "fcp_busy": "kopierer {0} filer...\n\n{1}", + "fp_abrt": "avbryter...", "fp_err": "flytting feilet:\n", "fcp_err": "kopiering feilet:\n", "fp_confirm": "flytt disse {0} filene hit?", @@ -1686,6 +1688,7 @@ var Ls = { "fcp_ok": "复制成功", //m "fp_busy": "正在移动 {0} 项...\n\n{1}", "fcp_busy": "正在复制 {0} 项...\n\n{1}", //m + "fp_abrt": "正在中止...", //m "fp_err": "移动失败:\n", "fcp_err": "复制失败:\n", //m "fp_confirm": "将这些 {0} 项移动到这里?", @@ -2319,6 +2322,7 @@ var Ls = { "fcp_ok": "kopírování OK", "fp_busy": "přesouvám {0} položek...\n\n{1}", "fcp_busy": "kopíruji {0} položek...\n\n{1}", + "fp_abrt": "přerušuji...", //m "fp_err": "přesun selhal:\n", "fcp_err": "kopírování selhalo:\n", "fp_confirm": "přesunout těchto {0} položek sem?", @@ -2948,6 +2952,7 @@ var Ls = { "fcp_ok": "Kopieren OK", "fp_busy": "Verschiebe {0} Elemente...\n\n{1}", "fcp_busy": "Kopiere {0} Elemente...\n\n{1}", + "fp_abrt": "Abbrechen...", //m "fp_err": "Verschieben fehlgeschlagen:\n", "fcp_err": "Kopieren fehlgeschlagen:\n", "fp_confirm": "Diese {0} Elemente hierher verschieben?", @@ -3577,6 +3582,7 @@ var Ls = { "fcp_ok": "kopiointi OK", "fp_busy": "siirretään {0} kohdetta...\n\n{1}", "fcp_busy": "kopioidaan {0} kohdetta...\n\n{1}", + "fp_abrt": "keskeytetään...", //m "fp_err": "siirto epäonnistui:\n", "fcp_err": "kopiointi epäonnistui:\n", "fp_confirm": "siirrä nämä {0} kohdetta tänne?", @@ -4206,6 +4212,7 @@ var Ls = { "fcp_ok": "copie OK", "fp_busy": "déplacement de {0} éléments…\n\n{1}", "fcp_busy": "copie de {0} éléments…\n\n{1}", + "fp_abrt": "abandon en cours...", //m "fp_err": "deplacement échoué:\n", "fcp_err": "copie échouée:\n", "fp_confirm": "déplacer ces {0} éléments ici ?", @@ -4835,6 +4842,7 @@ var Ls = { "fcp_ok": "αντιγραφή OK", "fp_busy": "μετακίνηση {0} αντικειμένων...\n\n{1}", "fcp_busy": "αντιγραφή {0} αντικειμένων...\n\n{1}", + "fp_abrt": "γίνεται ακύρωση...", //m "fp_err": "αποτυχία μετακίνησης:\n", "fcp_err": "αποτυχία αντιγραφής:\n", "fp_confirm": "να μετακινηθούν αυτά τα {0} αντικείμενα εδώ;", @@ -5464,6 +5472,7 @@ var Ls = { "fcp_ok": "copia OK", "fp_busy": "spostando {0} elementi...\n\n{1}", "fcp_busy": "copiando {0} elementi...\n\n{1}", + "fp_abrt": "annullamento in corso...", //m "fp_err": "spostamento fallito:\n", "fcp_err": "copia fallita:\n", "fp_confirm": "spostare questi {0} elementi qui?", @@ -6093,6 +6102,7 @@ var Ls = { "fcp_ok": "Kopiëren OK", "fp_busy": "{0} items verplaatsen...\n\n{1}", "fcp_busy": "{0} items kopiëren...\n\n{1}", + "fp_abrt": "afbreken...", //m "fp_err": "Verplaatsen mislukt:\n", "fcp_err": "Kopieëren mislukt:\n", "fp_confirm": "Verplaats deze {0} items hierheen?", @@ -6723,6 +6733,7 @@ var Ls = { "fcp_ok": "kopiering OK", "fp_busy": "flyttar {0} filer...\n\n{1}", "fcp_busy": "kopierar {0} filer...\n\n{1}", + "fp_abrt": "avbryt...", "fp_err": "flytting feila:\n", "fcp_err": "kopiering feila:\n", "fp_confirm": "flytt disse {0} filene hit?", @@ -7349,6 +7360,7 @@ var Ls = { "fcp_ok": "przekopiowano", "fp_busy": "przenoszenie {0} elementów...\n\n{1}", "fcp_busy": "kopiowanie {0} elementów...\n\n{1}", + "fp_abrt": "przerywanie...", //m "fp_err": "nie udało się przenieść:\n", "fcp_err": "nie udało się skopiować:\n", "fp_confirm": "przenieść tutaj {0} elementy(ów)?", @@ -7978,6 +7990,7 @@ var Ls = { "fcp_ok": "успешно скопировано", "fp_busy": "перемещаю {0} файлов...\n\n{1}", "fcp_busy": "копирую {0} файлов...\n\n{1}", + "fp_abrt": "прерывание...", //m "fp_err": "ошибка перемещения:\n", "fcp_err": "ошибка копирования:\n", "fp_confirm": "переместить эти {0} файлов сюда?", @@ -8606,6 +8619,7 @@ var Ls = { "fcp_ok": "copia correcta", "fp_busy": "moviendo {0} elementos...\n\n{1}", "fcp_busy": "copiando {0} elementos...\n\n{1}", + "fp_abrt": "cancelando...", //m "fp_err": "fallo al mover:\n", "fcp_err": "fallo al copiar:\n", "fp_confirm": "¿mover estos {0} elementos aquí?", @@ -9235,6 +9249,7 @@ var Ls = { "fcp_ok": "копіювання OK", "fp_busy": "переміщення {0} елементів...\n\n{1}", "fcp_busy": "копіювання {0} елементів...\n\n{1}", + "fp_abrt": "переривання...", //m "fp_err": "переміщення невдале:\n", "fcp_err": "копіювання невдале:\n", "fp_confirm": "перемістити ці {0} елементи сюди?", @@ -9882,6 +9897,7 @@ var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext), hash0 = location.hash, sloc0 = '' + location, noih = /[?&]v\b/.exec(sloc0), + abrt_key = "", rtt = null, ldks = [], dks = {}, @@ -12414,6 +12430,15 @@ function fmt_ren(re, md, fmt) { } +function fs_abrt() { + toast.inf(30, L.fp_abrt); + fileman.f.length = 0; + var xhr = new XHR(); + xhr.open('POST', '/?fs_abrt=' + abrt_key, true); + xhr.send(); +} + + var fileman = (function () { var bren = ebi('fren'), bdel = ebi('fdel'), @@ -12424,6 +12449,7 @@ var fileman = (function () { t_paste, r = {}; + r.f = []; r.clip = null; try { r.bus = new BroadcastChannel("fileman_bus"); @@ -12711,6 +12737,8 @@ var fileman = (function () { base = vsplit(sel[0].vp)[0], mkeys; + r.f = f; + for (var a = 0; a < sel.length; a++) { var vp = sel[a].vp; if (vp.endsWith('/')) @@ -12990,7 +13018,9 @@ var fileman = (function () { return rn_cancel(); } - toast.show('inf r', 0, esc(L.fr_busy.format(f.length, f[0].ofn))); + var msg = esc(L.fr_busy.format(f.length, f[0].ofn)); + msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>'; + toast.show('inf r', 0, msg); var dst = base + uricom_enc(f[0].inew.value, false); function rename_cb() { @@ -13004,8 +13034,10 @@ var fileman = (function () { return rn_apply_loop(); } + abrt_key = randstr(9); + var xhr = new XHR(); - xhr.open('POST', f[0].src + '?move=' + dst, true); + xhr.open('POST', f[0].src + '?move=' + dst + '&akey=' + abrt_key, true); xhr.onload = xhr.onerror = rename_cb; xhr.send(); } @@ -13267,6 +13299,8 @@ var fileman = (function () { srcdir = vsplit(r.clip[0])[0], links = QSA('#files tbody td:nth-child(2) a'); + r.f = f; + for (var a = 0, aa = links.length; a < aa; a++) indir.push(uricom_dec(vsplit(noq_href(links[a]))[1])); @@ -13298,13 +13332,17 @@ var fileman = (function () { if (!t.dst) return paster(); - toast.show('inf r', 0, esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src)))); + var msg = esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src))); + msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>'; + toast.show('inf r', 0, msg); var xhr = new XHR(), act = r.ccp ? '?copy=' : '?move=', dst = get_evpath() + uricom_enc(t.dst); - xhr.open('POST', t.src + act + dst, true); + abrt_key = randstr(9); + + xhr.open('POST', t.src + act + dst + '&akey=' + abrt_key, true); xhr.onload = xhr.onerror = paste_cb; xhr.send(); } diff --git a/tests/util.py b/tests/util.py index f02f81ba..bbd0b215 100644 --- a/tests/util.py +++ b/tests/util.py @@ -143,7 +143,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" + ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ka.update(**{k: False for k in ex.split()}) ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip" From d676a86f3fa5b7379ea2c2c495abe3e77f5ef1e7 Mon Sep 17 00:00:00 2001 From: Bevinsky <vin@r-a-d.io> Date: Thu, 14 Aug 2025 00:50:15 +0200 Subject: [PATCH 029/154] Add Swedish translation (#551) --- copyparty/web/browser.js | 632 ++++++++++++++++++++++++++++++++++++++- copyparty/web/splash.js | 42 +++ 2 files changed, 673 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 112e7da0..047c6446 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -8828,6 +8828,636 @@ var Ls = { "lang_set": "¿refrescar para que el cambio surta efecto?" }, + "swe": { + "tt": "Svenska", + + "cols": { + "c": "aktion", + "dur": "längd", + "q": "kvalitet / bitrate", + "Ac": "ljudkodek", + "Vc": "videokodek", + "Fmt": "format / container", + "Ahash": "ljudchecksumma", + "Vhash": "videochecksumma", + "Res": "upplösning", + "T": "filtyp", + "aq": "ljudkvalitet / bitrate", + "vq": "videokvalitet / bitrate", + "pixfmt": "subsampling / pixelstruktur", + "resw": "horisontell upplösning", + "resh": "vertikal upplösning", + "chs": "antal ljudkanaler", + "hz": "samplingsfrekvens" + }, + + "hks": [ + [ + "övrigt", + ["ESC", "stäng diverse paneler"], + + "filhanterare", + ["G", "växla mellan listvy / rutnät"], + ["T", "växla mellan miniatyrer / ikoner"], + ["⇧ A/D", "miniatyrstorlek"], + ["ctrl-K", "radera urval"], + ["ctrl-X", "klipp urval till urklipp"], + ["ctrl-C", "kopiera urval till urklipp"], + ["ctrl-V", "klistra in (kopiera/flytta) hit"], + ["Y", "ladda ner urval"], + ["F2", "byt namn på urval"], + + "välja filer", + ["Blanksteg", "växla val av fil"], + ["↑/↓", "flytta filvalsmarkör"], + ["ctrl ↑/↓", "flytta markör och vy"], + ["⇧ ↑/↓", "välj föregående/nästa fil"], + ["ctrl-A", "välj alla filer / mappar"], + ], [ + "navigation", + ["B", "växla mellan brödsmulor / trädvy"], + ["I/K", "föregående/nästa mapp"], + ["M", "hoppa till överordnad mapp (eller kollapsa närvarande)"], + ["V", "växla mellan mappar / textfiler i trädvy"], + ["A/D", "trädvystorlek"], + ], [ + "ljudspelare", + ["J/L", "föregående/nästa låt"], + ["U/O", "hoppa 10sek bakåt/framåt"], + ["0..9", "hoppa till 0%..90%"], + ["P", "play/paus (startar även uppspelning)"], + ["S", "välj låten som spelas"], + ["Y", "ladda ner låt"], + ], [ + "bildvisare", + ["J/L, ←/→", "föregående/nästa bild"], + ["Hem/End", "första/sista bilden"], + ["F", "helskärm"], + ["R", "rotera medsols"], + ["⇧ R", "rotera motsols"], + ["S", "välj bild"], + ["Y", "ladda ner bild"], + ], [ + "videospelare", + ["U/O", "hoppa 10sek bakåt/framåt"], + ["P/K/Blanksteg", "play/paus"], + ["C", "fortsätt med nästa"], + ["V", "loopa"], + ["M", "stäng av ljud"], + ["[ och ]", "ställ in loopintervall"], + ], [ + "textfilsvisare", + ["I/K", "föregående/nästa fil"], + ["M", "stäng textfil"], + ["E", "redigera textfil"], + ["S", "välj fil"], + ] + ], + + "m_ok": "OK", + "m_ng": "Avbryt", + + "enable": "Aktivera", + "danger": "VARNING", + "clipped": "kopierat till urklipp", + + "ht_s1": "sekund", + "ht_s2": "sekunder", + "ht_m1": "minut", + "ht_m2": "minuter", + "ht_h1": "timme", + "ht_h2": "timmar", + "ht_d1": "dag", + "ht_d2": "dagar", + "ht_and": " och ", + + "goh": "kontrollpanel", + "gop": 'föregående mapp">föreg.', + "gou": 'överordnad mapp">upp', + "gon": 'nästa mapp">nästa', + "logout": "Logga ut ", + "access": "-rättighet", + "ot_close": "stäng undermeny", + "ot_search": "sök efter filer via attribut, sökväg / namn, musiktaggar, eller någon kombination av dessa$N$N<code>foo bar</code> = måste innehålla både «foo» och «bar»,$N<code>foo -bar</code> = måste innehålla «foo» men inte «bar»,$N<code>^yana .opus$</code> = måste börja med «yana» och vara en «opus»-fil$N<code>"try unite"</code> = måste innehålla exakt «try unite»$N$Ndatumformatet är iso-8601, t.ex.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", + "ot_unpost": "unpost: radera dina senaste uppladdningar, eller avbryt pågående sådana", + "ot_bup": "bup: enkel uppladdare, stödjer t o m netscape 4.0", + "ot_mkdir": "mkdir: skapa en ny mapp", + "ot_md": "new-md: skapa ett nytt markdown-dokument", + "ot_msg": "msg: skicka ett meddelande till serverloggen", + "ot_mp": "mediaspelarinställningar", + "ot_cfg": "konfigurationsinställningar", + "ot_u2i": 'up2k: ladda upp filer (om du har skrivrättigheter) eller byt till sökläge för att se om de finns någonstans på servern$N$Nuppladdningarna är återupptagbara, multitrådade och filernas tidsstämpel bevaras, men den använder mer CPU än [🎈]  (den enkla uppladdaren)<br /><br />under uppladdningens gång blir denna ikon en förloppsindikator!', + "ot_u2w": 'up2k: ladda upp filer med stöd för återupptagning (stäng din webbläsare och dra in samma filer senare)$N$Nmultitrådad och filernas tidsstämpel bevaras, men den använder mer CPU än [🎈]  (den enkla uppladdaren)<br /><br />under uppladdningens gång blir denna ikon en förloppsindikator!', + "ot_noie": 'Var vänlig använd Chrome / Firefox / Edge', + + "ab_mkdir": "skapa mapp", + "ab_mkdoc": "nytt markdown-dokument", + "ab_msg": "skicka medd. till serverlogg", + + "ay_path": "hoppa till mappar", + "ay_files": "hoppa till filer", + + "wt_ren": "byt namn på urval$NSnabbtangent: F2", + "wt_del": "radera urval$NSnabbtangent: ctrl-K", + "wt_cut": "klipp urval<small>(för att klistra in någonstans)</small>$NSnabbtangent: ctrl-X", + "wt_cpy": "kopiera urval till urklipp$N(för att klistra in någonstans)$NSnabbtangent: ctrl-C", + "wt_pst": "klistra in tidigare urval$NSnabbtangent: ctrl-V", + "wt_selall": "välj alla filer$NSnabbtangent: ctrl-A (när en fil har fokus)", + "wt_selinv": "invertera urval", + "wt_zip1": "ladda ner denna mapp som ett arkiv", + "wt_selzip": "ladda ner urval som ett arkiv", + "wt_seldl": "ladda ner urval som separata filer$NSnabbtangent: Y", + "wt_npirc": "kopiera IRC-formatterad låtinfo", + "wt_nptxt": "kopiera låtinfo i klartext", + "wt_m3ua": "lägg till i m3u-spellista (klicka på <code>📻copy</code> senare)", + "wt_m3uc": "kopiera m3u-spellista till urklipp", + "wt_grid": "växla mellan rutnät och listvy$NSnabbtangent: G", + "wt_prev": "föregående låt$NSnabbtangent: J", + "wt_play": "play / paus$NSnabbtangent: P", + "wt_next": "nästa låt$NSnabbtangent: L", + + "ul_par": "samtidiga uppladdningar:", + "ut_rand": "slumpa filnamn", + "ut_u2ts": "bevara tidsstämpeln för senaste ändring$Nfrån ditt filsystem till servern\">📅", + "ut_ow": "skriv över existerande filer på servern?$N🛡️: aldrig (skapar ett nytt filnamn istället)$N🕒: skriv över om serverns fil är äldre än din$N♻️: skriv alltid över om filerna skiljer sig", + "ut_mt": "fortsätt hasha filer under uppladdningens gång$N$Nstäng av om din CPU eller disk är en flaskhals", + "ut_ask": 'bekräfta innan uppladdningar påbörjas">💭', + "ut_pot": "förbättra uppladdningshastigheten på långsamma enheter$Ngenom att förenkla användargränssnittet", + "ut_srch": "ladda inte upp; kolla istället om filerna redan existerar på $N servern (detta kommer att skanna alla mappar med läsrättighet)", + "ut_par": "du kan pausa all uppladdning genom att sätta detta till 0$N$Nöka denna om din uppkoppling är långsam eller har hög latens$N$Nsätt till 1 över lokala nätverk eller om serverns disk är en flaskhals", + "ul_btn": "släpp filer / mappar<br>här (eller klicka)", + "ul_btnu": "L A D D A U P P", + "ul_btns": "S Ö K", + + "ul_hash": "hashar", + "ul_send": "skickar", + "ul_done": "klar", + "ul_idle1": "inga uppladdningar har köats", + "ut_etah": "medelhastighet för <em>hashning</em>, och uppskattad återstående tid", + "ut_etau": "medelhastighet för <em>överföring</em>, och uppskattad återstående tid", + "ut_etat": "<em>total</em> medelhastighet, och uppskattad återstående tid", + + "uct_ok": "lyckade", + "uct_ng": "no-good: misslyckade / avvisade / ej funna", + "uct_done": "ok och ng kombinerat", + "uct_bz": "pågående", + "uct_q": "köade", + + "utl_name": "filnamn", + "utl_ulist": "visa", + "utl_ucopy": "kopiera", + "utl_links": "länkar", + "utl_stat": "status", + "utl_prog": "förlopp", + + // keep short: + "utl_404": "404", + "utl_err": "FEL", + "utl_oserr": "OS-fel", + "utl_found": "hittad", + "utl_defer": "väntar", + "utl_yolo": "YOLO", + "utl_done": "klar", + + "ul_flagblk": "filerna lades till i kön,</b><br>men det finns en upptagen up2k i en annan webbläsarflik,<br>så vi väntar på den först", + "ul_btnlk": "serverkonfigurationen har låst denna inställning", + + "udt_up": "Ladda upp", + "udt_srch": "Sök", + "udt_drop": "släpp här", + + "u_nav_m": '<h6>jaha, vad har du då?</h6><code>Enter</code> = Filer (en eller flera)\n<code>ESC</code> = En mapp (inklusive undermappar)', + "u_nav_b": '<a href="#" id="modal-ok">Filer</a><a href="#" id="modal-ng">En mapp</a>', + + "cl_opts": "växlar", + "cl_themes": "tema", + "cl_langs": "språk", + "cl_ziptype": "mappnedladdning", + "cl_uopts": "up2k-inställningar", + "cl_favico": "favikon", + "cl_bigdir": "stora mappar", + "cl_hsort": "#sort.", + "cl_keytype": "tonartsnotering", + "cl_hiddenc": "dolda kolumner", + "cl_hidec": "dölj", + "cl_reset": "återställ", + "cl_hpick": "tryck på en kolumntitel för att dölja den i filvyn", + "cl_hcancel": "kolumndöljning avbruten", + + "ct_grid": '田 rutnätet', + "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_ihop": 'skrolla till den senast visade filen när bildvisaren stängs">g⮯', + "ct_dots": 'visa dolda filer (om servern tillåter detta)">dolda', + "ct_qdel": 'bekräfta endast en gång när filer raderas">srad', + "ct_dir1st": 'sortera mappar före filer">📁 först', + "ct_nsort": 'naturlig sortering (för filnamn med ledande siffror)">nsort', + "ct_utc": 'visa alla datum och tider i UTC">UTC', + "ct_readme": 'visa README.md i listvyn">📜 läsmig', + "ct_idxh": 'visa index.html istället för listvyn">htm', + "ct_sbars": 'visa rullningslister">⟊', + + "cut_umod": "om en fil redan existerar på servern, uppdatera serverns senast modifierade tidsstämpel till att matcha din lokala fil (kräver skriv+radera-rättighet)\">re📅", + + "cut_turbo": "yolo-knappen, du vill förmodligen INTE aktivera denna:$N$Nanvänd denna om du höll på att ladda upp en stor mängd filer och var tvungen att stänga webbläsaren, och du vill fortsätta uppladdningen så fort som möjligt$N$Ndetta ersätter hash-checken med en enkel <em>"har denna fil samma filstorlek på servern?"</em>, så om filinnehållet skiljer sig kommer den INTE att laddas upp$N$Ndu bör stänga av denna när uppladdningen är klar och sedan "ladda upp" samma filer igen för att låta klienten verifiera dem\">turbo", + + "cut_datechk": "har endast effekt med turbo-växeln påslagen$N$Nminskar yolo-faktorn lite grann; kollar om filtidsstämplarna på servern matchar dina$N$Ndetta <em>bör</em> fånga de flesta ofärdiga / korrumperade uppladdningarna, men kan inte ersätta ett fullständigt verifieringspass med turbo avstängt\">date-chk", + + "cut_u2sz": "storlek (i MiB) för varje uppladdnings-chunk; stora värden flyger bättre över atlanten. Prova lägre värden på mycket opålitliga uppkopplingar", + + "cut_flag": "garantera att endast en flik laddar upp samtidigt $N -- andra flikar måste också ha denna påslagen -- $N påverkar endast flikar på samma domän", + + "cut_az": "ladda upp filer i alfabetisk ordning, snarare än mindre filer först$N$Nalfabetisk ordning kan göra det enklare att se var något har gått fel, men uppladdningen blir lite långsammare över fiber / lokala nätverk", + + "cut_nag": "skicka en systemnotifikation när uppladdningar blir klara$N(endast om webbläsaren eller fliken inte är fokuserade)", + "cut_sfx": "ljudnotifikation när uppladdningar blir klara$N(endast om webbläsaren eller fliken inte är fokuserade)", + + "cut_mt": "använd multitrådning för att accelerera filhashningen$N$Ndetta använder web-workers och kräver$Nmer RAM (upp till 512 MiB extra)$N$Nuppladdningar över https blir 30% snabbare, över http 4.5x snabbare\">mt", + + "cut_wasm": "använd wasm istället för webbläsarens inbyggda hashare; förbättrar hastigheten i chrome-baserade webbläsare men ökar CPU-lasten, och många äldre versioner av chrome har buggar som får webbläsaren att konsumera allt RAM-minne och krascha om detta är påslaget\">wasm", + + "cft_text": "favikon-text (låt stå tom och uppdatera sidan för att stänga av)", + "cft_fg": "förgrundsfärg", + "cft_bg": "bakgrundsfärg", + + "cdt_lim": "högsta antal filer att visa in en mapp", + "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", + + "tt_entree": "visa trädvy$NSnabbtangent: B", + "tt_detree": "visa brödsmulor$NSnabbtangent: B", + "tt_visdir": "skrolla till öppnad mapp", + "tt_ftree": "växla mellan trädvy och textfiler$NHotkey: V", + "tt_pdock": "visa överordnade mappar i en panel längst upp i vyn", + "tt_dynt": "väx vyn när trädet expanderar", + "tt_wrap": "automatisk radbrytning", + "tt_hover": "visa överlånga rader när muspekaren hovrar över dem$N( skrollhjulet fungerar ej såvida inte pekaren$Nstår till vänster )", + + "ml_pmode": "vid mappens slut...", + "ml_btns": "komm.", + "ml_tcode": "konvertera", + "ml_tcode2": "konvertera till", + "ml_tint": "hy", + "ml_eq": "ljudutjämnare", + "ml_drc": "dynamikkompressor", + + "mt_loop": "upprepa en låt\">🔁", + "mt_one": "stoppa uppspelningen efter en låt\">1️⃣", + "mt_shuf": "blanda låtarna i varje mapp\">🔀", + "mt_aplay": "spela automatiskt om det finns en låt-ID i länkar du har klickat på för att öppna sidan$N$Nom detta är avstängt kommer sidans adress inte att bli uppdaterad med en låt-ID om du spelar musik, för att förhindra automatisk uppspelning om dessa inställningar går förlorade men webbadressen återstår\">a▶", + "mt_preload": "påbörja nedladdning av nästa låt i förväg för gapfri uppspelning\">ladda", + "mt_prescan": "hoppa till nästa mapp i förväg så att webbläsaren$Nförblir glad och inte avbryter uppspelningen\">nav", + "mt_fullpre": "försök att ladda ner hela låten i förväg;$N✅ aktivera på <b>opålitliga</b> uppkopplingar,$N❌ <b>avaktivera</b> kanske på långsamma uppkopplingar\">full", + "mt_fau": "förhindra att uppspelningen avstannar på telefoner om nästa låt inte laddas tillräckligt snabbt i förväg (kan ge upphov till buggiga musiktaggar)\">☕️", + "mt_waves": "vågformsreglage:$Nvisa ljudstyrkan i uppspelningsreglaget\">~s", + "mt_npclip": "visa knappar för att kopiera låtinfo till urklippet\">/np", + "mt_m3u_c": "visa knappar för att kopiera de valda$Nlåtarna som en m3u8-spellista\">📻", + "mt_octl": "systemintegration (mediaknappar / skärmdisplay)\">os-ctl", + "mt_oseek": "tillåt fram- och bakåtspolning via systemintegrationen$N$Nobs.: på vissa enheter (iPhone)$Nersätter detta knappen för nästa låt\">spola", + "mt_oscv": "visa skivomslag i skärmdisplayen\">omslag", + "mt_follow": "skrolla vyn till den spelande låten\">🎯", + "mt_compact": "kompakt kontrollpanel\">⟎", + "mt_uncache": "rensa cachen  (prova detta om din webbläsare har cachat$Nen trasig kopia av en låt och den vägrar spela upp den)\">rensa", + "mt_mloop": "upprepa den öppna mappen\">🔁 upprepa", + "mt_mnext": "ladda nästa mapp och fortsätt\">📂 nästa", + "mt_mstop": "stoppa uppspelningen\">⏸ stopp", + "mt_cflac": "konvertera flac / wav till {0}\">flac", + "mt_caac": "konvertera aac / m4a till {0}\">aac", + "mt_coth": "konvertera allt annat (förutom mp3) till {0}\">annat", + "mt_c2opus": "bäst val för pc, laptop, android\">opus", + "mt_c2owa": "opus-weba, för iOS 17.5 och senare\">owa", + "mt_c2caf": "opus-caf, för iOS 11 till 17\">caf", + "mt_c2mp3": "använd detta på mycket gamla enheter\">mp3", + "mt_c2flac": "bäst ljudkvalitet, men enorma nedladdningar\">flac", + "mt_c2wav": "okomprimerad uppspelning (ännu större)\">wav", + "mt_c2ok": "snyggt, bra val", + "mt_c2nd": "det är inte det rekommenderade formatet för din enhet, men det är lungt", + "mt_c2ng": "din enhet verkar inte stödja det här formatet, men vi provar ändå", + "mt_xowa": "det finns buggar i iOS som hindrar uppspelning i bakgrunden med detta format; vänligen använd caf eller mp3 istället", + "mt_tint": "nivå på bakgrundsfärg (0-100) på uppspelningsreglaget;$Ngör buffring mindre distraherande", + "mt_eq": "aktiverar utjämning och förstärkning;$N$Nboost <code>0</code> = standard 100%-volym (omodifierad)$N$Nwidth <code>1  </code> = standard stereo (omodifierad)$Nwidth <code>0.5</code> = 50% vänster-höger crossfeed$Nwidth <code>0  </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = tar bort sång :^)$N$Nnär utjämningen är aktiverad blir gaplösa album verkligen gaplösa, så låt den stå påslagen med alla värden satta till 0 (förutom width = 1) om du bryr dig om det", + "mt_drc": "aktiverar dynamikkompressorn (volymtillplattning / brickwaller); aktiverar även utjämnaren för att balansera röran, så sätt alla fält i utjämnaren förutom 'width' till 0 om du inte vill ha den$N$Nsänker all volym över THRESHOLD dB; för varje RATIO dB över THRESHOLD blir det 1 dB av output, så standardvärdena tresh = -24 och ratio = 12 innebär att volymen aldrig bör bli högre än -22 dB och det är säkert att höja utjämnarens boost till 0.8, eller t.o.m. 1.8 med ATK 0 och ett högt RLS-värde t.ex. 90 (fungerar endast i firefox; RLS är låst till högst 1 i andra webbläsare)$N$N(se wikipedia för en bättre förklaring)", + + "mb_play": "play", + "mm_hashplay": "spela upp den här ljudfilen?", + "mm_m3u": "tryck <code>Enter/OK</code> för att spela\ntryck <code>ESC/Avbryt</code> to Edit", + "mp_breq": "firefox 82+ eller chrome 73+ eller iOS 15+ krävs", + "mm_bload": "laddar...", + "mm_bconv": "konverterar till {0}, vänligen vänta...", + "mm_opusen": "din webbläsare kan inte spela upp aac- eller m4a-filer;\nkonvertering till opus är nu påslaget", + "mm_playerr": "uppspelning misslyckades: ", + "mm_eabrt": "Uppspelningen avbröts", + "mm_enet": "Din uppkoppling är skum", + "mm_edec": "Filen är korrumperad??", + "mm_esupp": "Din webbläsare förstår inte detta format", + "mm_eunk": "Okänt Fel", + "mm_e404": "Kunde inte spela upp ljudfil; fel 404: Filen hittades inte.", + "mm_e403": "Kunde inte spela upp ljudfil; fel 403: Åtkomst nekad.\n\nProva att ladda om sidan med F5, du kanske blev utloggad", + "mm_e500": "Kunde inte spela upp ljudfil; fel 500: Kolla serverloggen.", + "mm_e5xx": "Kunde inte spela upp ljudfil; serverfel ", + "mm_nof": "hittade inga fler låtar i närheten", + "mm_prescan": "Letar efter fler låtar...", + "mm_scank": "Hittade nästa låt:", + "mm_uncache": "cachen rensad; alla låtar kommer att laddas ner igen vid uppspelning", + "mm_hnf": "den låten finns inte längre", + + "im_hnf": "den bilden finns inte längre", + + "f_empty": 'mappen är tom', + "f_chide": 'detta kommer att dölja kolumnen «{0}»\n\ndu kan visa kolumner igen i inställningarna', + "f_bigtxt": "den här filen är {0} MiB stor -- vill du verkligen visa den som text?", + "f_bigtxt2": "visa endast slutet på filen? detta aktiverar även övervakning, vilket visar nya rader i filen i realtid", + "fbd_more": '<div id="blazy"><code>{0}</code> av <code>{1}</code> filer visas; <a href="#" id="bd_more">visa {2}</a> eller <a href="#" id="bd_all">visa alla</a></div>', + "fbd_all": '<div id="blazy"><code>{0}</code> av <code>{1}</code> filer visas; <a href="#" id="bd_all">visa alla</a></div>', + "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_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 <code>.PARTIAL</code>-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 <code>.PARTIAL</code>-filen istället att laddas ner, vilket är nästan garanterat att ge dig korrumperad data.", + + "ft_paste": "klistra in {0} objekt$NSnabbtangent: ctrl-V", + "fr_eperm": 'kan ej byta namn:\ndu har inte flytträttighet i denna mapp', + "fd_eperm": 'kan ej radera:\ndu har inte raderingsrättighet i denna mapp', + "fc_eperm": 'kan ej klippa:\ndu har inte flytträttighet i denna mapp', + "fp_eperm": 'kan ej klistra in:\ndu har inte skrivrättighet i denna mapp', + "fr_emore": "välj minst en fil att byta namn på", + "fd_emore": "välj minst en fil att radera", + "fc_emore": "välj minst en fil att klippa", + "fcp_emore": "välj minst en fil att kopiera till urklippet", + + "fs_sc": "dela den öppna mappen", + "fs_ss": "dela de urvalda filerna", + "fs_just1d": "du kan inte välja mer än en mapp\neller blanda filer och mappar i samma urval", + "fs_abrt": "❌ avbryt", + "fs_rand": "🎲 slump.namn", + "fs_go": "✅ skapa utdelning", + "fs_name": "namn", + "fs_src": "källa", + "fs_pwd": "lösen", + "fs_exp": "utgång", + "fs_tmin": "min", + "fs_thrs": "timmar", + "fs_tdays": "dagar", + "fs_never": "oändlig", + "fs_pname": "valfritt länknamn; slumpas fram om detta står tomt", + "fs_tsrc": "filen eller mappen att dela", + "fs_ppwd": "valfritt lösenord", + "fs_w8": "skapar utdelning...", + "fs_ok": "tryck <code>Enter/OK</code> för att kopiera länken till urklipp\ntryck <code>ESC/Avbryt</code> för att stänga", + + "frt_dec": "kan laga vissa typer av trasiga filnamn\">avkoda-url", + "frt_rst": "återställ modifierade filnamn till de ursprungliga\">↺ återställ", + "frt_abrt": "avbryt och stäng denna panel\">❌ avbryt", + "frb_apply": "BYT NAMN", + "fr_adv": "batch-, metadata- och mönsteromskrivning\">avancerat", + "fr_case": "skiftlägeskänsligt reguljärt uttryck\">skift", + "fr_win": "windows-säkra namn; ersätt <code><>:"\\|?*</code> med japanska fullbreddtecken\">win", + "fr_slash": "ersätt <code>/</code> med ett tecken som inte skapar nya mappar\">ingen /", + "fr_re": "reguljärt sökuttryck att tillämpa på ursprungliga filnamn; grupper kan hänvisas till i formatfältet nedan via <code>(1)</code> och <code>(2)</code> osv.", + "fr_fmt": "inspirerat av foobar2000:$N<code>(title)</code> ersätts av låttitel,$N<code>[(artist) - ](title)</code> skippar [detta] om artisten är tom$N<code>$lpad((tn),2,0)</code> fyller i spårnumret till 2 siffror", + "fr_pdel": "ta bort", + "fr_pnew": "spara som", + "fr_pname": "ge ett nytt namn på din inställning", + "fr_aborted": "avbrutet", + "fr_lold": "gammalt namn", + "fr_lnew": "nytt namn", + "fr_tags": "taggar för de valda filerna (skrivskyddat, endast som referens):", + "fr_busy": "byter namn på {0} objekt...\n\n{1}", + "fr_efail": "namnbyte misslyckades:\n", + "fr_nchg": "{0} av de nya namnen ändrades p g a <code>win</code> och/eller <code>ingen /</code>\n\nÄr det okej att fortsätta med de nya namnen?", + + "fd_ok": "radering lyckades", + "fd_err": "radering misslyckades:\n", + "fd_none": "inget raderades; kanske blockerat av serverkonfigurationen (xbd)?", + "fd_busy": "raderar {0} objekt...\n\n{1}", + "fd_warn1": "RADERA dessa {0} objekt?", + "fd_warn2": "<b>Sista chansen!</b> Det finns inget sätt att ångra detta. Radera?", + + "fc_ok": "klippte {0} objekt", + "fc_warn": 'klippte {0} objekt, men:\n\nendast <b>denna</b> webbläsarflik kan klistra in dem\n(eftersom urvalet är så enormt stort)', + + "fcc_ok": "kopierade {0} objekt till urklippet", + "fcc_warn": 'kopierade {0} objekt till urklippet, men:\n\nendast <b>denna</b> webbläsarflik kan klistra in dem\n(eftersom urvalet är så enormt stort)', + + "fp_apply": "använd dessa namn", + "fp_ecut": "klipp eller kopiera filer / mappar först för att klistra / flytta dem\n\nobs.: du kan klippa och klistra mellan webbläsarflikar", + "fp_ename": "{0} objekt kan ej flyttas hit eftersom filnamnen redan är tagna. Ge dem nya namn nedan för att fortsätta, eller lämna fältet tomt för att skippa:", + "fcp_ename": "{0} objekt kan ej kopieras hit eftersom filnamnen redan är tagna. Ge dem nya namn nedan för att fortsätta, eller lämna fältet tomt för att skippa:", + "fp_emore": "det finns fortfarande filnamnskrockar att fixa", + "fp_ok": "flytt lyckades", + "fcp_ok": "kopiering lyckades", + "fp_busy": "flyttar {0} objekt...\n\n{1}", + "fcp_busy": "kopierar {0} objekt...\n\n{1}", + "fp_abrt": "avbryter...", + "fp_err": "flytt misslyckades:\n", + "fcp_err": "kopiering misslyckades:\n", + "fp_confirm": "flytta dessa {0} objekt hit?", + "fcp_confirm": "kopiera dessa {0} objekt hit?", + "fp_etab": 'lyckades ej läsa urklippet från en annan webbläsarflik', + "fp_name": "laddar upp en fil från din enhet. Ge den ett namn:", + "fp_both_m": '<h6>välj vad som ska klistras in</h6><code>Enter</code> = Flytta {0} objekt från «{1}»\n<code>ESC</code> = Ladda upp {2} filer från din enhet', + "fcp_both_m": '<h6>välj vad som ska klistras in</h6><code>Enter</code> = Kopiera {0} objekt från «{1}»\n<code>ESC</code> = Ladda upp {2} filer från din enhet', + "fp_both_b": '<a href="#" id="modal-ok">Flytta</a><a href="#" id="modal-ng">Ladda upp</a>', + "fcp_both_b": '<a href="#" id="modal-ok">Kopiera</a><a href="#" id="modal-ng">Ladda upp</a>', + + "mk_noname": "skriv ett namn i fältet till vänster först :p", + + "tv_load": "Laddar textfil:\n\n{0}\n\n{1}% ({2} av {3} MiB laddat)", + "tv_xe1": "kunde ej ladda textfil:\n\nfel ", + "tv_xe2": "404, filen hittades inte", + "tv_lst": "lista av textfiler i", + "tvt_close": "återvänd till mapp$NSnabbtangent: M (eller Esc)\">❌ stäng", + "tvt_dl": "ladda ner denna fil$NSnabbtangent: Y\">💾 ladda ner", + "tvt_prev": "visa föregående fil$NSnabbtangent: i\">⬆ föreg.", + "tvt_next": "visa nästa fil$NSnabbtangent: K\">⬇ nästa", + "tvt_sel": "välj fil   ( för klipp / kopiera / radera / ... )$NSnabbtangent: S\">välj", + "tvt_edit": "öppna fil i textredigerare$NSnabbtangent: E\">✏️ redigera", + "tvt_tail": "övervaka filen; visa nya rader i realtid\">📡 övervaka", + "tvt_wrap": "automatisk radbrytning\">↵", + "tvt_atail": "lås vyn till sidans botten\">⚓", + "tvt_ctail": "avkoda terminalfärger (ansi-escapesekvenser)\">🌈", + "tvt_ntail": "gräns för scrollback (hur många byte ska behållas laddade)", + + "m3u_add1": "låt tillagd till m3u-spellista", + "m3u_addn": "{0} låtar tillagda till m3u-spellista", + "m3u_clip": "m3u-spellista kopierad till urklippet\n\ndu bör skapa en ny textfil som heter någonting.m3u och klistra in spellistan i det dokumentet; detta gör den uppspelbar", + + "gt_vau": "visa inte videor, spela endast ljudet\">🎧", + "gt_msel": "urval av filer; ctrl-klicka en fil för standardbeteende$N$N<em>när detta är aktiverat: dubbelklicka en fil / mapp för att öppna den</em>$N$NSnabbtangent: S\">urval", + "gt_crop": "centrera och beskär miniatyrbilder\">beskär", + "gt_3x": "högupplösta miniatyrbilder\">3x", + "gt_zoom": "zoom", + "gt_chop": "klipp", + "gt_sort": "sortera efter", + "gt_name": "namn", + "gt_sz": "storlek", + "gt_ts": "datum", + "gt_ext": "typ", + "gt_c1": "förkorta filnamn (visa mindre)", + "gt_c2": "förläng filnamn (visa mer)", + + "sm_w8": "söker...", + "sm_prev": "sökresultaten nedan är från en tidigare sökning:\n ", + "sl_close": "stäng sökresultaten", + "sl_hits": "visar {0} träffar", + "sl_moar": "ladda fler", + + "s_sz": "storlek", + "s_dt": "datum", + "s_rd": "sökväg", + "s_fn": "namn", + "s_ta": "taggar", + "s_ua": "uppl.", + "s_ad": "avanc.", + "s_s1": "minimum MiB", + "s_s2": "maximum MiB", + "s_d1": "min. iso8601", + "s_d2": "max. iso8601", + "s_u1": "uppladdad efter", + "s_u2": "och/eller före", + "s_r1": "sökvägen innehåller   (blankstegsseparerat)", + "s_f1": "filnamnet innehåller   (invertera med -intedetta)", + "s_t1": "taggar innehåller   (^=start, slut=$)", + "s_a1": "specifika metadataegenskaper", + + "md_eshow": "kan ej visa ", + "md_off": "[📜<em>läsmig</em>] avstängt i [⚙️] -- dokumentet är dolt", + + "badreply": "Kunde ej tolka svaret från servern", + + "xhr403": "403: Åtkomst nekad\n\nProva att ladda om sidan med F5, du kanske blev utloggad", + "xhr0": "okänt (tappade förmodligen kontakt med servern, eller så är den nere)", + "cf_ok": "ledsen -- DD" + wah + "oS-skyddet har aktiverats\n\nsaker bör fungera igen om 30 sekunder\n\nom inget händer, ladda om sidan med F5", + "tl_xe1": "kunde inte visa undermappar:\n\nfel ", + "tl_xe2": "404: Mappen hittades inte", + "fl_xe1": "kunde inte visa filer i mapp:\n\nfel ", + "fl_xe2": "404: Mappen hittades inte", + "fd_xe1": "kunde inte skapa mapp:\n\nfel ", + "fd_xe2": "404: Överordnad mapp hittades inte", + "fsm_xe1": "kunde inte skicka meddelande:\n\ndel ", + "fsm_xe2": "404: Överordnad mapp hittades inte", + "fu_xe1": "kunde inte ladda unpost-listan från servern:\n\nfel ", + "fu_xe2": "404: Filen hittades inte??", + + "fz_tar": "okomprimerad tar-fil i gnu-format (linux / mac)", + "fz_pax": "okomprimerad tar-fil i pax-format (långsammare)", + "fz_targz": "gnu-tar komprimerad med gzip-nivå 3$N$Ndetta är vanligtvis mycket långsamt,$Nanvänd okomprimerad tar istället", + "fz_tarxz": "gnu-tar komprimerad med xz-nivå 1$N$Ndetta är vanligtvis mycket långsamt,$Nanvänd okomprimerad tar istället", + "fz_zip8": "zip-fil med utf8-filnman (kan vara skum i windows 7 och äldre)", + "fz_zipd": "zip-fil med standard cp437-filnamn, för riktigt gammal mjukvara", + "fz_zipc": "cp437 med crc32 uträknad i förväg,$Nför MS-DOS PKZIP v2.04g (oktober 1993)$N(tar längre tid att behandla innan nedladdningen kan påbörjas)", + + "un_m1": "du kan radera dina senaste uppladdningar (eller avbryta pågående sådana) nedan", + "un_upd": "uppdatera", + "un_m4": "eller, dela filerna som syns nedan:", + "un_ulist": "visa", + "un_ucopy": "kopiera", + "un_flt": "filter:  sökvägen måste innehålla", + "un_fclr": "rensa filtret", + "un_derr": 'unpost-radering misslyckades:\n', + "un_f5": 'något gick sönder, prova att uppdatera eller tryck på F5', + "un_uf5": "ledsen men du måste uppdatera sidan (t.ex. genom att trycka på F5 eller CTRL-R) innan du kan avbryta den här uppladdningen", + "un_nou": '<b>varning:</b> servern är för upptagen för att visa pågående uppladdningar; klicka på "uppdatera" om en stund', + "un_noc": '<b>varning:</b> serverkonfigurationen tillåter inte unpost:ning av uppladdade filer', + "un_max": "visar de första 2000 filerna (använd filtret)", + "un_avail": "{0} av de senaste uppladdningarna kan raderas<br />{1} pågående uppladdningar kan avbrytas", + "un_m2": "sorterat efter uppladdningstid; senast uppladdad först:", + "un_no1": "tjosan! inga uppladdningar är tillräckligt nya", + "un_no2": "tjosan! inga uppladdningar som matchar filtret är tillräckligt nya", + "un_next": "radera de {0} nästkommande filerna", + "un_abrt": "avbryt", + "un_del": "radera", + "un_m3": "laddar dina senaste uppladdningar...", + "un_busy": "raderar {0} filer...", + "un_clip": "{0} länkar kopierade till urklippet", + + "u_https1": "du bör", + "u_https2": "byta till https", + "u_https3": "för bättre prestanda", + "u_ancient": 'din webbläsare är imponerande uråldrig -- du kanske borde <a href="#" onclick="goto(\'bup\')">använda bup istället</a>', + "u_nowork": "firefox 53+ eller chrome 57+ eller iOS 11+ krävs", + "tail_2old": "firefox 105+ eller chrome 71+ eller iOS 14.5+ krävs", + "u_nodrop": 'din webbläsare är för gammal för dra-och-släpp-uppladdning', + "u_notdir": "det där är ingen mapp!\n\ndin webbläsare är för gammal,\nprova dra-och-släpp istället", + "u_uri": "släpp bilder från andra webbläsarfönster på den stora\nuppladdningsknappen för att ladda upp dem", + "u_enpot": 'byt till <a href="#">potatisgränssnittet</a> (kan förbättra uppladdningshastigheten)', + "u_depot": 'byt till <a href="#">det snygga gränssnittet</a> (kan försämra uppladdningshastigheten)', + "u_gotpot": 'byter till potatisgränssnittet för förbättrad uppladdningshastighet,\n\nbyt gärna tillbaka om du vill!', + "u_pott": "<p>filer:   <b>{0}</b> färdiga,   <b>{1}</b> misslyckade,   <b>{2}</b> pågående,   <b>{3}</b> köade</p>", + "u_ever": "detta är den enkla uppladdaren; up2k kräver minst<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", + "u_su2k": 'detta är den enkla uppladdaren; <a href="#" id="u2yea">up2k</a> är bättre', + "u_uput": 'optimera hastigheten (skippa checksumman)', + "u_ewrite": 'du har inte skrivrättighet i denna mapp', + "u_eread": 'du har inte läsrättighet i denna mapp', + "u_enoi": 'serverkonfigurationen har inte slagit på sökning', + "u_enoow": "du kan inte skriva över här; raderingsrättighet krävs", + "u_badf": 'Dessa {0} filer (av totalt {1}) skippades, möjligtvis p.g.a. filsystemsrättigheter:\n\n', + "u_blankf": 'Dessa {0} filer (av totalt {1}) är tomma; ladda upp dem ändå?\n\n', + "u_applef": 'Dessa {0} filer (av totalt {1}) är förmodligen oönskade;\nTryck <code>OK/Enter</code> för att SKIPPA de följande filerna,\nTryck <code>Avbryt/ESC</code> för att INKLUDERA och LADDA UPP dem:\n\n', + "u_just1": '\nDet kanske fungerar om du endast väljer en fil', + "u_ff_many": "om du använder <b>Linux / MacOS / Android,</b> så <em>kan</em> denna mängd filer <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">krascha Firefox!</a>\nom detta händer, vänligen försök igen (eller använd Chrome).", + "u_up_life": "Denna uppladdning kommer att raderas från servern om\n{0} efter att den har blivit uppladdad", + "u_asku": 'ladda upp dessa {0} filer till <code>{1}</code>', + "u_unpt": "du kan ångra / radera denna uppladdning med 🧯 uppe till vänster", + "u_bigtab": 'försöker att visa {0} filer\n\ndetta kan krascha din webbläsare, är du säker?', + "u_scan": 'Scannar filer...', + "u_dirstuck": 'katalogskannern fastnade när den försökte komma åt de följande {0} objekten; dessa kommer att skippas:', + "u_etadone": 'Klar ({0}, {1} filer)', + "u_etaprep": '(förbereder uppladdning)', + "u_hashdone": 'hashning klar', + "u_hashing": 'hashar', + "u_hs": 'skakar hand...', + "u_started": "filerna laddas nu upp; se [🚀]", + "u_dupdefer": "duplikat; kommer att behandlas efter alla andra filer", + "u_actx": "klicka här för att undvika prestandaförlust<br />när du byter till andra fönster/flikar", + "u_fixed": "Okej!  Fixat 👍", + "u_cuerr": "misslyckades att ladda upp chunk {0} av {1};\nförmodligen harmlöst, fortsätter\n\nfil: {2}", + "u_cuerr2": "servern avvisade uppladdningen (chunk {0} av {1});\nprovar igen senare\n\nfil: {2}\n\nfel ", + "u_ehstmp": "provar igen; see nedåt till höger", + "u_ehsfin": "servern avvisade förfrågan att färdigställa uppladdningen; provar igen...", + "u_ehssrch": "servern avvisade förfrågan att söka; provar igen...", + "u_ehsinit": "servern avvisade förfrågan att påbörja uppladdningen; provar igen...", + "u_eneths": "nätverksfel vid handskakning; provar igen...", + "u_enethd": "nätverksfel när destinationens existens testades; provar igen...", + "u_cbusy": "väntar på att servern ska lita på oss igen efter nätverksfel...", + "u_ehsdf": "servern fick slut på diskutrymme!\n\nprovar igen, ifall någon rensar upp\ntillräckligt med utrymme för att fortsätta", + "u_emtleak1": "det verkar som att din webbläsare kanske har en minnesläcka;\nvänligen", + "u_emtleak2": ' <a href="{0}">byt till https (rekommenderat)</a> eller ', + "u_emtleak3": ' ', + "u_emtleakc": 'prova följande:\n<ul><li>tryck <code>F5</code> för att uppdatera sidan</li><li>avaktivera sedan  <code>mt</code> -växeln i  <code>⚙️-inställningarna</code></li><li>och prova att ladda upp igen</li></ul>Uppladdningar kommer att vara lite långsammare, men aja.\nBeklagar problemet!\n\nPS: chrome v107 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1354816" target="_blank">har en buggfix</a> för detta', + "u_emtleakf": 'prova följande:\n<ul><li>tryck <code>F5</code> för att uppdatera sidan</li><li>aktivera sedan <code>🥔</code> (potatis) i uppladdningsgränssnittet<li>och prova att ladda upp igen</li></ul>\nPS: firefox <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1790500" target="_blank">kommer förhoppningsvis få en buggfix</a> vid något tillfälle', + "u_s404": "hittades ej på servern", + "u_expl": "förklara", + "u_maxconn": "de flesta webbläsare begränsar detta till 6, men firefox låter dig höja gränsen med <code>connections-per-server</code> i <code>about:config</code>", + "u_tu": '<p class="warn">VARNING: turbo är aktiverat, <span> det är möjligt att klienten inte upptäcker och återupptar ofärdiga uppladdningar; se tipset för turbo-växeln</span></p>', + "u_ts": '<p class="warn">VARNING: turbo är aktiverat, <span> sökresultat kan vara felaktiga; se tipset för turbo-växeln</span></p>', + "u_turbo_c": "serverkonfigurationen har avaktiverat turbo", + "u_turbo_g": "avaktiverar turbo eftersom du inte har rättigheten\natt se mappars innehåll i den här volymen", + "u_life_cfg": 'radera automatiskt efter <input id="lifem" p="60" /> min (eller <input id="lifeh" p="3600" /> timmar)', + "u_life_est": 'uppladdningen kommer att raderas vid <span id="lifew" tt="local time">---</span>', + "u_life_max": 'denna mapp tvingar en\nhögsta livstid på {0}', + "u_unp_ok": 'unpost är tillåten för {0}', + "u_unp_ng": 'unpost är INTE tillåten', + "ue_ro": 'du har endast läsrättighet till denna mapp\n\n', + "ue_nl": 'du är inte inloggad', + "ue_la": 'du är inloggad som "{0}"', + "ue_sr": 'du är i filsökläge\n\nbyt till uppladdningsläge genom att klicka på förstoringsglaset 🔎 (bredvid den stora SÖK-knappen), och försök ladda upp igen\n\nledsen', + "ue_ta": 'prova att ladda upp igen nu, det bör fungera', + "ue_ab": "denna fil laddas redan upp till en annan mapp, och den uppladdningen måste färdigställas innan filen kan laddas upp någon annanstans.\n\nDu kan avbryta och glömma bort den uppladdningen med 🧯 uppe till vänster", + "ur_1uo": "Okej: Filen laddades upp med framgång", + "ur_auo": "Okej: Alla {0} filer laddades upp med framgång", + "ur_1so": "Okej: Filen fanns på servern", + "ur_aso": "Okej: Alla {0} filer fanns på servern", + "ur_1un": "Uppladdningen misslyckades, ledsen", + "ur_aun": "Alla {0} uppladdningar misslyckades, ledsen", + "ur_1sn": "Filen hittades INTE på servern", + "ur_asn": "De {0} filerna hittades INTE på servern", + "ur_um": "Klar;\n{0} uppladdningar gick okej,\n{1} uppladdningar misslyckades, ledsen", + "ur_sm": "Klar;\n{0} filer hittades på servern,\n{1} filer hittades INTE på servern", + + "lang_set": "uppdatera för att ändringen ska ta effekt?", + }, "ukr": { "tt": "Українська", @@ -9460,7 +10090,7 @@ var Ls = { }, }; -var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "nld", "rus", "spa", "ukr"]; +var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "nld", "rus", "spa", "swe", "ukr"]; if (window.langmod) langmod(); diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 8cbca1e6..3229b9a2 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -510,6 +510,48 @@ var Ls = { "af1": "mostrar subidas recientes", "ag1": "mostrar usuarios IdP conocidos" }, + "swe": { + "a1": "uppdatera", + "b1": "tjena främling   <small>(du är inte inloggad)</small>", + "c1": "logga ut", + "d1": "dumpa stacken", + "d2": "visar tillståndet på alla aktiva trådar", + "e1": "ladda om konfig.", + "e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart", + "f1": "du kan bläddra:", + "g1": "du kan ladda upp till:", + "cc1": "annat:", + "h1": "avaktivera k304", + "i1": "aktivera k304", + "j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet", + "k1": "återställ klientinställningar", + "l1": "logga in för att se mer:", + "m1": "välkommen tillbaka,", + "n1": "404 hittades inte  ┐( ´ -`)┌", + "o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>', + "p1": "403 nekat  ~┻━┻", + "q1": 'använd ett lösenord eller <a href="' + SR + '/?h">åk hem</a>', + "r1": "åk hem", + ".s1": "skanna om", + "t1": "åtgärd", + "u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder", + "v1": "koppla upp", + "v2": "använd denna server som en lokal disk", + "w1": "byt till https", + "x1": "byt lösenord", + "y1": "redigera utdelningar", + "z1": "lås upp denna utdelning:", + "ta1": "fyll i ditt nya lösenord", + "ta2": "upprepa det nya lösenordet:", + "ta3": "det blev fel; vänligen försök igen", + "aa1": "inkommande filer:", + "ab1": "avaktivera no304", + "ac1": "aktivera no304", + "ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!", + "ae1": "aktiva nedladdningar:", + "af1": "visa senaste uppladdningar", + "ag1": "visa idp-cache" + }, "ukr": { "a1": "оновити", "b1": "привітик, незнайомцю   <small>(ви не авторизовані)</small>", From 659f351c65237ce0a30319dacc6ad127308cc09f Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 16:42:48 +0000 Subject: [PATCH 030/154] support pillow-heif; closes #607 --- README.md | 4 ++-- copyparty/th_srv.py | 5 ++++- docs/devnotes.md | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b3a2d134..c8520202 100644 --- a/README.md +++ b/README.md @@ -2793,7 +2793,7 @@ enable [music tags](#metadata-from-audio-files): enable [thumbnails](#thumbnails) of... * **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+) * **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH` -* **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler) +* **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif` * **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+ * **JPEG XL pictures:** `pyvips` or `ffmpeg` * **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats) @@ -2827,7 +2827,7 @@ set any of the following environment variables to disable its associated optiona | `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg | | `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails | | `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) | -| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pyheif-pillow-opener/) | +| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pillow-heif/) | | `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow | | `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows | | `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images | diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 945c6d56..537c748a 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -86,7 +86,10 @@ try: if os.environ.get("PRTY_NO_PIL_HEIF"): raise Exception() - from pyheif_pillow_opener import register_heif_opener + try: + from pillow_heif import register_heif_opener + except ImportError: + from pyheif_pillow_opener import register_heif_opener register_heif_opener() HAVE_HEIF = True diff --git a/docs/devnotes.md b/docs/devnotes.md index 9fbb29df..e2fcc008 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -354,7 +354,7 @@ pip install mutagen # audio metadata pip install pyftpdlib # ftp server pip install partftpy # tftp server pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows -pip install Pillow pyheif-pillow-opener # thumbnails +pip install Pillow pillow-heif # thumbnails pip install pyvips # faster thumbnails pip install psutil # better cleanup of stuck metadata parsers on windows pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling From 6303effe59fb4004a075332f9487ae1b6d4c8b3a Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 17:49:48 +0000 Subject: [PATCH 031/154] configurable max num cookies --- copyparty/__main__.py | 2 ++ copyparty/httpcli.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 86d8cdd5..22e219a7 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1363,6 +1363,8 @@ def add_safety(ap): ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy") + ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies") + ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header") ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0") ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]") ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index a7ff303d..479c4f07 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -562,7 +562,7 @@ class HttpCli(object): zso = self.headers.get("cookie") if zso: - if len(zso) > 8192: + if len(zso) > self.args.cookie_cmax: self.loud_reply("cookie header too big", status=400) return False zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x] @@ -570,11 +570,15 @@ class HttpCli(object): cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or "" if "b" in cookies and "b" not in uparam: uparam["b"] = cookies["b"] + if len(cookies) > self.args.cookie_nmax: + self.loud_reply("too many cookies", status=400) else: cookies = {} cookie_pw = "" - if len(uparam) > 10 or len(cookies) > 50: + if len(uparam) > 12: + t = "http-request rejected; num.params: %d %r" + self.log(t % (len(uparam), self.req), 3) self.loud_reply("u wot m8", status=400) return False From 554cc2f3eebe8ff4ad1fa3488d78a30510d5e7df Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 18:09:23 +0000 Subject: [PATCH 032/154] support xdev/xvol in rootless vfs; closes #603 --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 479c4f07..c262dc2e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -704,7 +704,7 @@ class HttpCli(object): cookies["b"] = "" vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) - if "xdev" in vn.flags or "xvol" in vn.flags: + if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags): ap = vn.canonical(rem) avn = vn.chk_ap(ap) else: From 5b627425125e0c3c372e009859d251d889871f72 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 18:10:55 +0000 Subject: [PATCH 033/154] run from src with py3.9 --- copyparty/mtag.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 660f8180..43e9dd75 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -368,7 +368,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ return zd, md -def get_cover_from_epub(log: "NamedLogger", abspath: str) -> IO[bytes] | None: +def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]: import zipfile from .dxml import parse_xml @@ -418,7 +418,9 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> IO[bytes] | None: return z.open(adjusted_cover_path) -def _get_cover_from_epub2(log: "NamedLogger", package_root, package_ns) -> str | None: +def _get_cover_from_epub2( + log: "NamedLogger", package_root, package_ns +) -> Optional[str]: # <meta name="cover" content="id-to-cover-image"> in <metadata>, then # <item> in <manifest> cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get( From c4a4fddd272f31944a7f3615c4f3c62da5265756 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 18:36:31 +0000 Subject: [PATCH 034/154] translations: OK/Cancel; closes #599 --- copyparty/web/browser.js | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 047c6446..000edbb7 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1613,7 +1613,7 @@ var Ls = { "f_dls": '当前文件夹中的文件链接已\n更改为下载链接', - "f_partial": "要安全下载正在上传的文件,请点击没有 <code>.PARTIAL</code> 文件扩展名的同名文件。请按取消或 Escape 执行此操作。\n\n按 OK / Enter 将忽略此警告并继续下载 <code>.PARTIAL</code> 临时文件,这几乎肯定会导致数据损坏。", + "f_partial": "要安全下载正在上传的文件,请点击没有 <code>.PARTIAL</code> 文件扩展名的同名文件。请按取消或 Escape 执行此操作。\n\n按 确定 / Enter 将忽略此警告并继续下载 <code>.PARTIAL</code> 临时文件,这几乎肯定会导致数据损坏。", "ft_paste": "粘贴 {0} 项$N快捷键: ctrl-V", "fr_eperm": '无法重命名:\n你在此文件夹中没有 “移动” 权限', @@ -2214,7 +2214,7 @@ var Ls = { "mb_play": "přehrát", "mm_hashplay": "přehrát tento audio soubor?", - "mm_m3u": "stiskněte <code>Enter/OK</code> pro Přehrání\nstiskněte <code>ESC/Cancel</code> pro Úpravu", + "mm_m3u": "stiskněte <code>Enter/OK</code> pro Přehrání\nstiskněte <code>ESC/Zrušit</code> pro Úpravu", "mp_breq": "potřebujete firefox 82+ nebo chrome 73+ nebo iOS 15+", "mm_bload": "nyní se načítá...", "mm_bconv": "převádí se na {0}, čekejte prosím...", @@ -2247,7 +2247,7 @@ var Ls = { "f_dls": 'odkazy na soubory v aktuální složce byly\nzměněny na odkazy ke stažení', - "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 <code>.PARTIAL</code>. Stiskněte prosím CANCEL nebo Escape.\n\nStisknutím OK / Enter ignorujete toto varování a pokračujete ve stahování <code>.PARTIAL</code> dočasného souboru, což téměř jistě vyústí jako poškozená data.", + "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 <code>.PARTIAL</code>. Stiskněte prosím Zrušit nebo Escape.\n\nStisknutím OK / Enter ignorujete toto varování a pokračujete ve stahování <code>.PARTIAL</code> dočasného souboru, což téměř jistě vyústí jako poškozená data.", "ft_paste": "vložit {0} položek$NKlávesová zkratka: ctrl-V", "fr_eperm": 'nelze přejmenovat:\nnemáte oprávnění “přesunout” v této složce', @@ -2467,7 +2467,7 @@ var Ls = { "u_enoow": "přepsání zde nebude fungovat; je vyžadováno oprávnění k mazání", "u_badf": "Těchto {0} souborů (z celkem {1}) bylo přeskočeno, pravděpodobně kvůli oprávněním v souborovém systému:\n\n", "u_blankf": "Těchto {0} souborů (z celkem {1}) je prázdných; přesto je nahrát?\n\n", - "u_applef": "Těchto {0} souborů (z celkem {1}) je pravděpodobně nežádoucích;\nStiskněte <code>OK/Enter</code> pro PŘESKOČENÍ následujících souborů,\nStiskněte <code>Cancel/ESC</code> pro Zahrnutí a NAHRÁNÍ i těchto souborů:\n\n", + "u_applef": "Těchto {0} souborů (z celkem {1}) je pravděpodobně nežádoucích;\nStiskněte <code>OK/Enter</code> pro PŘESKOČENÍ následujících souborů,\nStiskněte <code>Zrušit/ESC</code> pro Zahrnutí a NAHRÁNÍ i těchto souborů:\n\n", "u_just1": "\nMožná to bude fungovat lépe, když vyberete pouze jeden soubor", "u_ff_many": "pokud používáte <b>Linux / MacOS / Android,</b> takové množství souborů <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>může</em> shodit Firefox!</a>\npokud se to stane, zkuste to prosím znovu (nebo použijte Chrome).", "u_up_life": "Tento upload bude smazán ze serveru\n{0} po jeho dokončení", @@ -3537,7 +3537,7 @@ var Ls = { "fs_tsrc": "jaettava tiedosto tai hakemisto", "fs_ppwd": "valinnainen salasana", "fs_w8": "luodaan sharea...", - "fs_ok": "paina <code>Enter/OK</code> lisätäksesi leikepöydälle\npaina <code>ESC/Cancel</code> sulkeaksesi", + "fs_ok": "paina <code>Enter/OK</code> lisätäksesi leikepöydälle\npaina <code>ESC/Peruuta</code> sulkeaksesi", "frt_dec": "saattaa korjata joitakin rikkinäisiä tiedostonimiä\">url-decode", "frt_rst": "palauta muokatut tiedostonimet takaisin alkuperäisiksi\">↺ palauta", @@ -3727,7 +3727,7 @@ var Ls = { "u_enoow": "ylikirjoitus ei toimi täällä; tarvitaan “Delete”-oikeus", "u_badf": 'Nämä {0} tiedostoa ({1} yhteensä) ohitettiin, mahdollisesti tiedostojärjestelmän oikeuksien vuoksi:\n\n', "u_blankf": 'Nämä {0} tiedostoa ({1} yhteensä) ovat tyhjiä; ladataanko ne silti?\n\n', - "u_applef": 'Nämä {0} tiedostoa ({1} yhteensä) ovat todennäköisesti ei-toivottuja;\nPaina <code>OK/Enter</code> OHITTAAKSESI seuraavat tiedostot,\nPaina <code>Cancel/ESC</code> jos ET halua sulkea pois, ja LATAA nekin:\n\n', + "u_applef": 'Nämä {0} tiedostoa ({1} yhteensä) ovat todennäköisesti ei-toivottuja;\nPaina <code>OK/Enter</code> OHITTAAKSESI seuraavat tiedostot,\nPaina <code>Peruuta/ESC</code> jos ET halua sulkea pois, ja LATAA nekin:\n\n', "u_just1": '\nEhkä toimii paremmin jos valitset vain yhden tiedoston', "u_ff_many": "jos käytät <b>Linux / MacOS / Android,</b> niin tämä määrä tiedostoja <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>saattaa</em> kaataa Firefoxin!</a>\njos niin käy, kokeile uudelleen (tai käytä Chromea).", "u_up_life": "Tämä lataus poistetaan palvelimelta\n{0} sen valmistumisen jälkeen", @@ -4137,7 +4137,7 @@ var Ls = { "f_dls": 'le lien de fichier dans le répertoire actuel\nà été changé en lien de téléchargement', - "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 <code>.PARTIAL</code>. Choisissez CANCEL 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 <code>.PARTIAL</code> à la place, ce qui donnera presque certainement des données corrompues.", + "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 <code>.PARTIAL</code>. 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 <code>.PARTIAL</code> à la place, ce qui donnera presque certainement des données corrompues.", "ft_paste": "coller {0} éléments$NHotkey: ctrl-V", "fr_eperm": 'impossible de renommer:\n vous n\'avez pas la permission “move” dans ce dossier', @@ -4357,7 +4357,7 @@ var Ls = { "u_enoow": "l'écrasage ne fonctionnera pas ici; besoin de permissions de suppression", "u_badf": 'Ces {0} fichiers (sur {1} au total) ont été ignorés, probablement en raison de permissions système de fichiers:\n\n', "u_blankf": 'Ces {0} fichiers (sur {1} au total) sont vides; les téléverser quand même ?\n\n', - "u_applef": 'Ces {0} fichiers (sur {1} au total) sont probablement indésirables;\nAppuyez sur <code>OK/Enter</code> pour IGNORER les fichiers suivants,\nAppuyez sur <code>Cancel/Échap</code> pour NE PAS exclure, et TÉLÉVERSER ceux-ci également:\n\n', + "u_applef": 'Ces {0} fichiers (sur {1} au total) sont probablement indésirables;\nAppuyez sur <code>OK/Enter</code> pour IGNORER les fichiers suivants,\nAppuyez sur <code>Annuler/Échap</code> pour NE PAS exclure, et TÉLÉVERSER ceux-ci également:\n\n', "u_just1": '\nPeut-être que cela fonctionne mieux si vous sélectionnez juste un fichier', "u_ff_many": "si vous utilisez <b>Linux / MacOS / Android,</b> alors ce nombre de fichiers <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>peut</em> faire planter Firefox!</a>\nSi cela se produit, veuillez réessayer (ou utiliser Chrome).", "u_up_life": "Ce téléversement va être supprimé du serveur\n{0} après son achèvement", @@ -4734,7 +4734,7 @@ var Ls = { "mb_play": "παίξε", "mm_hashplay": "να παίξω αυτό το αρχείο ήχου;", - "mm_m3u": "πάτα <code>Enter/OK</code> για Αναπαραγωγή\nπάτα <code>ESC/Άκυρο</code> για Επεξεργασία", + "mm_m3u": "πάτα <code>Enter/Εντάξει</code> για Αναπαραγωγή\nπάτα <code>ESC/Άκυρο</code> για Επεξεργασία", "mp_breq": "χρειάζεται firefox 82+ ή chrome 73+ ή iOS 15+", "mm_bload": "φορτώνει...", "mm_bconv": "μετατροπή σε {0}, περίμενε...", @@ -4767,7 +4767,7 @@ var Ls = { "f_dls": 'οι σύνδεσμοι αρχείων στον τρέχοντα φάκελο έχουν\nμετατραπεί σε συνδέσμους λήψης', - "f_partial": "Για να κατεβάσεις με ασφάλεια ένα αρχείο που ανεβαίνει, κλίκαρε το αρχείο με το ίδιο όνομα, αλλά χωρίς την κατάληξη <code>.PARTIAL</code>. Πάτα Άκυρο ή Escape για να σταματήσεις.\n\nΠάτα OK / Enter αν αγνοείς την προειδοποίηση και κατέβασε το <code>.PARTIAL</code> αρχείο, που σχεδόν σίγουρα θα είναι κατεστραμμένο.", + "f_partial": "Για να κατεβάσεις με ασφάλεια ένα αρχείο που ανεβαίνει, κλίκαρε το αρχείο με το ίδιο όνομα, αλλά χωρίς την κατάληξη <code>.PARTIAL</code>. Πάτα Άκυρο ή Escape για να σταματήσεις.\n\nΠάτα Εντάξει / Enter αν αγνοείς την προειδοποίηση και κατέβασε το <code>.PARTIAL</code> αρχείο, που σχεδόν σίγουρα θα είναι κατεστραμμένο.", "ft_paste": "επικόλλησε {0} αντικείμενα$NΠλήκτρο συντόμευσης: ctrl-V", "fr_eperm": 'δεν μπορεί να μετονομαστεί:\nδεν έχεις δικαίωμα “μετακίνησης” σε αυτόν το φάκελο', @@ -4797,7 +4797,7 @@ var Ls = { "fs_tsrc": "το αρχείο ή ο φάκελος προς κοινή χρήση", "fs_ppwd": "προαιρετικός κωδικός", "fs_w8": "δημιουργία κοινής χρήσης...", - "fs_ok": "πάτα <code>Enter/OK</code> για Πρόχειρο\nπάτα <code>ESC/Άκυρο</code> για Κλείσιμο", + "fs_ok": "πάτα <code>Enter/Εντάξει</code> για Πρόχειρο\nπάτα <code>ESC/Άκυρο</code> για Κλείσιμο", "frt_dec": "μπορεί να διορθώσει μερικές περιπτώσεις κατεστραμμένων ονομάτων αρχείων\">αποκωδικοποίηση url", "frt_rst": "επανέφερε τα ονόματα αρχείων στα αρχικά τους\">↺ επαναφορά", @@ -4987,7 +4987,7 @@ var Ls = { "u_enoow": "δεν μπορείς να κάνεις αντικατάσταση εδώ· χρειάζεται δικαίωμα Διαγραφής", "u_badf": "Αυτά τα {0} αρχεία (από {1} συνολικά) παραλείφθηκαν, πιθανώς λόγω δικαιωμάτων συστήματος αρχείων:\n\n", "u_blankf": "Αυτά τα {0} αρχεία (από {1} συνολικά) είναι άδεια / κενά· να τα μεταφορτώσω έτσι κι αλλιώς;\n\n", - "u_applef": "Αυτά τα {0} αρχεία (από {1} συνολικά) πιθανώς δεν είναι επιθυμητά;\nΠάτα <code>OK/Enter</code> για ΝΑ ΑΓΝΟΗΘΟΥΝ τα παρακάτω αρχεία,\nΠάτα <code>Cancel/ESC</code> για ΝΑ ΜΗΝ ΑΠΟΚΛΕΙΣΤΟΥΝ και να ΜΕΤΑΦΟΡΤΩΘΟΎΝ κι αυτά:\n\n", + "u_applef": "Αυτά τα {0} αρχεία (από {1} συνολικά) πιθανώς δεν είναι επιθυμητά;\nΠάτα <code>Εντάξει/Enter</code> για ΝΑ ΑΓΝΟΗΘΟΥΝ τα παρακάτω αρχεία,\nΠάτα <code>Άκυρο/ESC</code> για ΝΑ ΜΗΝ ΑΠΟΚΛΕΙΣΤΟΥΝ και να ΜΕΤΑΦΟΡΤΩΘΟΎΝ κι αυτά:\n\n", "u_just1": "\nΊσως δουλέψει καλύτερα αν επιλέξεις μόνο ένα αρχείο", "u_ff_many": "αν χρησιμοποιείς <b>Linux / MacOS / Android,</b> τότε τόσα αρχεία <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>μπορεί</em> να κατάρρευση του Firefox!</a>\nαν γίνει αυτό, δοκίμασε ξανά (ή χρησιμοποίησε τον Chrome).", "u_up_life": "Αυτή η μεταφόρτωση θα διαγραφεί από το διακομιστή\n{0} μετά την ολοκλήρωσή της", @@ -5994,7 +5994,7 @@ var Ls = { "mb_play": "Afspelen", "mm_hashplay": "Deze audio bestand afspelen?", - "mm_m3u": "Druk op <code>Enter/OK</code> om af te spelen\nDruk op <code>ESC/Cancel</code> om te bewerken", + "mm_m3u": "Druk op <code>Enter/OK</code> om af te spelen\nDruk op <code>ESC/Annuleren</code> om te bewerken", "mp_breq": "Heeft firefox 82+ of chrome 73+ of iOS 15+", "mm_bload": "Aan het laden...", "mm_bconv": "Opmzetten naar {0}, even geduld...", @@ -6057,7 +6057,7 @@ var Ls = { "fs_tsrc": "Het bestand of de map die u wilt delen", "fs_ppwd": "Optioneel wachtwoord", "fs_w8": "Delen...", - "fs_ok": "Druk op <code>Enter/OK</code> naar klembord te zetten\Druk op <code>ESC/Cancel</code> om te sluiten", + "fs_ok": "Druk op <code>Enter/OK</code> naar klembord te zetten\Druk op <code>ESC/Annuleren</code> om te sluiten", "frt_dec": "Kan sommige gevallen van gebroken bestandsnamen oplossen\">url-decode", "frt_rst": "Gewijzigde bestandsnamen terugzetten naar de oorspronkelijke namen\">↺ reset", @@ -6247,7 +6247,7 @@ var Ls = { "u_enoow": "Overschrijven zal hier niet werken; je heb verwijder toestemming nodig", "u_badf": 'Deze {0} bestanden (van {1} totaal) zijn overgeslagen, mogelijk door bestandssysteemmachtigingen:\n\n', "u_blankf": 'Deze {0} bestanden (van {1} totaal) zijn leeg; alsnog uploaden?\n\n', - "u_applef": 'Deze {0} bestanden (van {1} totaal) zijn waarschijnlijk ongewenst;\nKlik op <code>OK/Enter</code> om de volgende bestanden over te slaan,\Klik op <code>Cancel/ESC</code> niet uit te sluiten en deze ook te uploaden:\n\n', + "u_applef": 'Deze {0} bestanden (van {1} totaal) zijn waarschijnlijk ongewenst;\nKlik op <code>OK/Enter</code> om de volgende bestanden over te slaan,\Klik op <code>Annuleren/ESC</code> niet uit te sluiten en deze ook te uploaden:\n\n', "u_just1": '\nMisschien werkt het beter als je slechts één bestand selecteert', "u_ff_many": "Als je <b>Linux / MacOS / Android,</b> gebruikt dan <em>kan</em> deze hoeveelheid bestanden <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">Firefox crashen!</a>\nals dat gebeurt, probeer het dan opnieuw (of gebruik Chrome).", "u_up_life": "Deze upload wordt verwijderd van de server\n{0} nadat het is voltooid", @@ -7252,7 +7252,7 @@ var Ls = { "mb_play": "odtwórz", "mm_hashplay": "odtworzyć ten plik audio?", - "mm_m3u": "naciśnij <code>Enter/OK</code>, aby odtworzyć\nnaciśnij <code>ESC/Cancel</code>, aby edytować", + "mm_m3u": "naciśnij <code>Enter/OK</code>, aby odtworzyć\nnaciśnij <code>ESC/Anuluj</code>, aby edytować", "mp_breq": "wymagany jest Firefox 82+, Chrome 73+ lub iOS 15+", "mm_bload": "wczytywanie...", "mm_bconv": "konwertowanie do {0}, proszę czekać...", @@ -7945,7 +7945,7 @@ var Ls = { "fs_tsrc": "путь к файлу или папке, которыми нужно поделиться", "fs_ppwd": "пароль (необязательно)", "fs_w8": "создаю доступ...", - "fs_ok": "нажмите <code>Enter/OK</code>, чтобы скопировать\nнажмите <code>ESC/Cancel</code>, чтобы закрыть", + "fs_ok": "нажмите <code>Enter/OK</code>, чтобы скопировать\nнажмите <code>ESC/Отмена</code>, чтобы закрыть", "frt_dec": "может исправить некоторые случаи с некорректными именами файлов\">декодировать url", "frt_rst": "сбросить изменённые имена обратно к оригинальным\">↺ сброс", @@ -9771,7 +9771,7 @@ var Ls = { "mb_play": "відтворити", "mm_hashplay": "відтворити цей аудіо файл?", - "mm_m3u": "натисніть <code>Enter/OK</code> для відтворення\nнатисніть <code>ESC/Cancel</code> для редагування", + "mm_m3u": "натисніть <code>Enter/Гаразд</code> для відтворення\nнатисніть <code>ESC/Скасувати</code> для редагування", "mp_breq": "потрібен firefox 82+ або chrome 73+ або iOS 15+", "mm_bload": "зараз завантажується...", "mm_bconv": "конвертується в {0}, будь ласка, зачекайте...", @@ -9804,7 +9804,7 @@ var Ls = { "f_dls": 'посилання на файли в поточній папці були\nзмінені на посилання для завантаження', - "f_partial": "Щоб безпечно завантажити файл, який зараз завантажується, будь ласка, клацніть на файл, який має таке саме ім'я, але без розширення <code>.PARTIAL</code>. Будь ласка, натисніть CANCEL або Escape, щоб зробити це.\n\nНатиснення OK / Enter проігнорує це попередження і продовжить завантаження <code>.PARTIAL</code> робочого файлу замість цього, що майже напевно дасть вам пошкоджені дані.", + "f_partial": "Щоб безпечно завантажити файл, який зараз завантажується, будь ласка, клацніть на файл, який має таке саме ім'я, але без розширення <code>.PARTIAL</code>. Будь ласка, натисніть Скасувати або Escape, щоб зробити це.\n\nНатиснення Гаразд / Enter проігнорує це попередження і продовжить завантаження <code>.PARTIAL</code> робочого файлу замість цього, що майже напевно дасть вам пошкоджені дані.", "ft_paste": "вставити {0} елементів$NГаряча клавіша: ctrl-V", "fr_eperm": 'не можу перейменувати:\nу вас немає дозволу “переміщення“ в цій папці', @@ -9834,7 +9834,7 @@ var Ls = { "fs_tsrc": "файл або папка для спільного доступу", "fs_ppwd": "необов'язковий пароль", "fs_w8": "створення спільного доступу...", - "fs_ok": "натисніть <code>Enter/OK</code> для копіювання до буфера\nнатисніть <code>ESC/Cancel</code> для закриття", + "fs_ok": "натисніть <code>Enter/Гаразд</code> для копіювання до буфера\nнатисніть <code>ESC/Скасувати</code> для закриття", "frt_dec": "може виправити деякі випадки пошкоджених імен файлів\">url-decode", "frt_rst": "скинути змінені імена файлів назад до оригінальних\">↺ reset", @@ -10024,7 +10024,7 @@ var Ls = { "u_enoow": "перезапис не працюватиме тут; потрібен дозвіл на видалення", "u_badf": 'Ці {0} файли (з {1} загальних) були пропущені, можливо, через дозволи файлової системи:\n\n', "u_blankf": 'Ці {0} файли (з {1} загальних) порожні; все одно завантажити їх?\n\n', - "u_applef": 'Ці {0} файли (з {1} загальних), ймовірно, небажані;\nНатисніть <code>OK/Enter</code> щоб ПРОПУСТИТИ наступні файли,\nНатисніть <code>Cancel/ESC</code> щоб НЕ виключати, і ЗАВАНТАЖИТИ їх також:\n\n', + "u_applef": 'Ці {0} файли (з {1} загальних), ймовірно, небажані;\nНатисніть <code>Гаразд/Enter</code> щоб ПРОПУСТИТИ наступні файли,\nНатисніть <code>Скасувати/ESC</code> щоб НЕ виключати, і ЗАВАНТАЖИТИ їх також:\n\n', "u_just1": '\nМожливо, це спрацює краще, якщо ви виберете лише один файл', "u_ff_many": "якщо ви використовуєте <b>Linux / MacOS / Android,</b> то така кількість файлів <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>може</em> завісити Firefox!</a>\nякщо це станеться, будь ласка, спробуйте знову (або використовуйте Chrome).", "u_up_life": "Це завантаження буде видалено з сервера\n{0} після його завершення", From 68907eaf487ff83b07daea9f25d600fdd6573628 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 19:11:57 +0000 Subject: [PATCH 035/154] add "@acct", a group with all authed users; closes #604 --- README.md | 3 +++ copyparty/__main__.py | 4 ++++ copyparty/authsrv.py | 4 ++++ tests/util.py | 3 +++ 4 files changed, 14 insertions(+) diff --git a/README.md b/README.md index c8520202..16bfb15f 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,8 @@ examples: * replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it * replacing the `g` permission with `wG` would let anonymous users upload files, receiving a working direct link in return +if you want to grant access to all users who are logged in, the group `acct` will always contain all known users, so for example `-v /mnt/music:music:r,@acct` + anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf` @@ -538,6 +540,7 @@ and if you want to use config files instead of commandline args (good!) then her accs: r: u1, u2 # only these accounts can read, r: @g1 # (exactly the same, just with a group instead) + r: @acct # (alternatively, ALL users who are logged in) rw: u3 # and only u3 can read-write [/inc] diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 22e219a7..38f786d8 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -609,6 +609,9 @@ def get_sects(): if no accounts or volumes are configured, current folder will be read/write for everyone + the group @acct will always have every user with an account + (the name of that group can be changed with --grp-all) + consider the config file for more flexible account/volume management, including dynamic reload at runtime (and being more readable w) """ @@ -1163,6 +1166,7 @@ def add_auth(ap): ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)") ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies") + ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known") ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 0f1c94f9..3bea55c8 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1099,6 +1099,9 @@ class AuthSrv(object): if rejected: continue + if gn == self.args.grp_all: + gn = "" + # if ap/vp has a user/group placeholder, make sure to keep # track so the same user/group is mapped when setting perms; # otherwise clear un/gn to indicate it's a regular volume @@ -1208,6 +1211,7 @@ class AuthSrv(object): self.load_idp_db(bool(self.idp_accs)) ret = {un: gns[:] for un, gns in self.idp_accs.items()} ret.update({zs: [""] for zs in acct if zs not in ret}) + grps[self.args.grp_all] = list(ret.keys()) for gn, uns in grps.items(): for un in uns: try: diff --git a/tests/util.py b/tests/util.py index bbd0b215..16c75e92 100644 --- a/tests/util.py +++ b/tests/util.py @@ -185,9 +185,12 @@ class Cfg(Namespace): E=E, bup_ck="sha512", chmod_d="755", + cookie_cmax=8192, + cookie_nmax=50, dbd="wal", dk_salt="b" * 16, fk_salt="a" * 16, + grp_all="acct", idp_gsep=re.compile("[|:;+,]"), iobuf=256 * 1024, lang="eng", From 7f448750610426740c39da5db4e64184899a9fd2 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 19:22:04 +0000 Subject: [PATCH 036/154] autogen pw for blank-pw users (closes #596); if a user is defined with a blank password, generate a strong password for that user --- copyparty/authsrv.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 3bea55c8..63613cc8 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1885,6 +1885,16 @@ class AuthSrv(object): if LEELOO_DALLAS in all_users: raise Exception("sorry, reserved username: " + LEELOO_DALLAS) + zsl = [] + for usr in list(acct)[:]: + zs = acct[usr].strip() + if not zs: + zs = ub64enc(os.urandom(48)).decode("ascii") + zsl.append(usr) + acct[usr] = zs + if zsl: + self.log("generated random passwords for users %r" % (zsl,), 6) + seenpwds = {} for usr, pwd in acct.items(): if pwd in seenpwds: From 4e878d2f1e2231027040de65cdb1cd97878f670c Mon Sep 17 00:00:00 2001 From: nyqui <67160376+nyqui@users.noreply.github.com> Date: Fri, 15 Aug 2025 04:30:41 +0900 Subject: [PATCH 037/154] initial Korean translation (#583) Signed-off-by: nyqui <67160376+nyqui@users.noreply.github.com> --- copyparty/web/browser.js | 631 ++++++++++++++++++++++++++++++++++++++- copyparty/web/splash.js | 42 +++ 2 files changed, 672 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 000edbb7..a387495e 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -5681,6 +5681,635 @@ var Ls = { "lang_set": "aggiornare per rendere effettivo il cambiamento?", }, + "kor": { + "tt": "한국어", + + "cols": { + "c": "작업 버튼", + "dur": "길이", + "q": "품질/비트레이트", + "Ac": "오디오 코덱", + "Vc": "비디오 코덱", + "Fmt": "형식/컨테이너", + "Ahash": "오디오 체크섬", + "Vhash": "비디오 체크섬", + "Res": "해상도", + "T": "파일 유형", + "aq": "오디오 품질/비트레이트", + "vq": "비디오 품질/비트레이트", + "pixfmt": "서브샘플링/픽셀 구조", + "resw": "가로 해상도", + "resh": "세로 해상도", + "chs": "오디오 채널", + "hz": "샘플레이트" + }, + + "hks": [ + [ + "기타", + ["ESC", "다양한 창 닫기"], + + "파일 관리자", + ["G", "목록/그리드 보기 전환"], + ["T", "썸네일/아이콘 전환"], + ["⇧ A/D", "썸네일 이미지 크기"], + ["ctrl-K", "선택 항목 삭제"], + ["ctrl-X", "선택 항목 잘라내기"], + ["ctrl-C", "선택 항목 복사"], + ["ctrl-V", "여기에 붙여넣기 (이동/복사)"], + ["Y", "선택 항목 다운로드"], + ["F2", "선택 항목 이름 바꾸기"], + + "파일 목록 선택", + ["space", "파일 선택/해제"], + ["↑/↓", "선택 커서 이동"], + ["ctrl ↑/↓", "커서와 뷰포트 동시 이동"], + ["⇧ ↑/↓", "이전/다음 파일 선택"], + ["ctrl-A", "모든 파일/폴더 선택"], + ], [ + "탐색", + ["B", "브레드크럼/탐색창 전환"], + ["I/K", "이전/다음 폴더"], + ["M", "상위 폴더 (또는 현재 항목 닫기)"], + ["V", "탐색창에 폴더/텍스트 파일 표시 전환"], + ["A/D", "탐색창 크기"], + ], [ + "오디오 플레이어", + ["J/L", "이전/다음 곡"], + ["U/O", "10초 뒤로/앞으로 건너뛰기"], + ["0..9", "0%..90% 지점으로 이동"], + ["P", "재생/일시정지 (시작 포함)"], + ["S", "재생 중인 곡 선택"], + ["Y", "곡 다운로드"], + ], [ + "이미지 뷰어", + ["J/L, ←/→", "이전/다음 이미지"], + ["Home/End", "첫/마지막 이미지"], + ["F", "전체 화면"], + ["R", "시계 방향으로 회전"], + ["⇧ R", "반시계 방향으로 회전"], + ["S", "이미지 선택"], + ["Y", "이미지 다운로드"], + ], [ + "비디오 플레이어", + ["U/O", "10초 뒤로/앞으로 건너뛰기"], + ["P/K/Space", "재생/일시정지"], + ["C", "다음 파일 계속 재생"], + ["V", "반복"], + ["M", "음소거"], + ["[ 와 ]", "반복 구간 설정"], + ], [ + "텍스트 파일 뷰어", + ["I/K", "이전/다음 파일"], + ["M", "텍스트 파일 닫기"], + ["E", "텍스트 파일 편집"], + ["S", "파일 선택 (잘라내기/복사/이름 바꾸기용)"], + ] + ], + + "m_ok": "확인", + "m_ng": "취소", + + "enable": "활성화", + "danger": "위험", + "clipped": "클립보드에 복사되었습니다", + + "ht_s1": "초", + "ht_s2": "초", + "ht_m1": "분", + "ht_m2": "분", + "ht_h1": "시간", + "ht_h2": "시간", + "ht_d1": "일", + "ht_d2": "일", + "ht_and": " ", + + "goh": "제어판", + "gop": '이전 형제 폴더">이전', + "gou": '상위 폴더">위로', + "gon": '다음 폴더">다음', + "logout": "로그아웃 ", + "access": " 액세스", + "ot_close": "하위 메뉴 닫기", + "ot_search": "속성, 경로/이름, 음악 태그 또는 이들의 조합으로 파일을 검색합니다.$N$N<code>foo bar</code> = «foo»와 «bar»를 모두 포함해야 함,$N<code>foo -bar</code> = «foo»는 포함하지만 «bar»는 포함하지 않아야 함,$N<code>^yana .opus$</code> = «yana»로 시작하고 «opus» 파일이어야 함$N<code>"try unite"</code> = 정확히 «try unite»를 포함해야 함$N$N날짜 형식은 ISO-8601입니다. 예:$N<code>2009-12-31</code> 또는 <code>2020-09-12 23:30:00</code>", + "ot_unpost": "주워담기: 최근 업로드한 항목을 삭제하거나 미완료된 업로드를 중단합니다", + "ot_bup": "bup: 기본 업로더. 넷스케이프 4.0도 지원합니다", + "ot_mkdir": "mkdir: 새 디렉터리를 만듭니다", + "ot_md": "new-md: 새 마크다운 문서를 만듭니다", + "ot_msg": "msg: 서버 로그에 메시지를 보냅니다", + "ot_mp": "미디어 플레이어 옵션", + "ot_cfg": "구성 옵션", + "ot_u2i": 'up2k: (쓰기 권한이 있는 경우) 파일을 업로드하거나, 검색 모드로 전환하여 서버 어딘가에 파일이 있는지 확인합니다.$N$N업로드는 재개 가능하고, 멀티스레드로 작동하며, 파일 타임스탬프가 보존되지만, [🎈] (기본 업로더)보다 CPU를 더 많이 사용합니다.<br /><br />업로드 중에는 이 아이콘이 진행률 표시창이 됩니다!', + "ot_u2w": 'up2k: 이어올리기 기능을 지원하는 파일 업로더입니다 (브라우저를 닫았다가 나중에 동일한 파일을 끌어다 놓으세요).$N$N멀티스레드로 작동하며, 파일 타임스탬프가 보존되지만, [🎈] (기본 업로더)보다 CPU를 더 많이 사용합니다.<br /><br />업로드 중에는 이 아이콘이 진행률 표시창이 됩니다!', + "ot_noie": 'Chrome / Firefox / Edge를 사용해주세요', + + "ab_mkdir": "디렉터리 만들기", + "ab_mkdoc": "새 마크다운 문서", + "ab_msg": "서버 로그에 메시지 보내기", + + "ay_path": "폴더로 건너뛰기", + "ay_files": "파일로 건너뛰기", + + "wt_ren": "선택한 항목 이름 바꾸기$N단축키: F2", + "wt_del": "선택한 항목 삭제$N단축키: ctrl-K", + "wt_cut": "선택한 항목 잘라내기 <small>(다른 곳에 붙여넣기용)</small>$N단축키: ctrl-X", + "wt_cpy": "선택한 항목 클립보드에 복사$N(다른 곳에 붙여넣기용)$N단축키: ctrl-C", + "wt_pst": "이전에 잘라내거나 복사한 항목 붙여넣기$N단축키: ctrl-V", + "wt_selall": "모든 파일 선택$N단축키: ctrl-A (파일에 포커스된 경우)", + "wt_selinv": "선택 반전", + "wt_zip1": "이 폴더를 압축 파일로 다운로드", + "wt_selzip": "선택 항목을 압축 파일로 다운로드", + "wt_seldl": "선택 항목을 개별 파일로 다운로드$N단축키: Y", + "wt_npirc": "IRC 형식 트랙 정보 복사", + "wt_nptxt": "일반 텍스트 트랙 정보 복사", + "wt_m3ua": "m3u 재생 목록에 추가 (나중에 <code>📻복사</code> 클릭)", + "wt_m3uc": "m3u 재생 목록을 클립보드에 복사", + "wt_grid": "그리드/목록 보기 전환$N단축키: G", + "wt_prev": "이전 트랙$N단축키: J", + "wt_play": "재생/일시정지$N단축키: P", + "wt_next": "다음 트랙$N단축키: L", + + "ul_par": "동시 업로드:", + "ut_rand": "파일명 무작위로 만들기", + "ut_u2ts": "사용자 파일 시스템의 마지막 수정 타임스탬프를$N서버에 복사\">📅", + "ut_ow": "서버에 있는 기존 파일을 덮어쓸까요?$N🛡️: 안 함 (대신 새 파일 이름 생성)$N🕒: 서버 파일이 더 오래된 경우 덮어쓰기$N♻️: 파일이 다르면 항상 덮어쓰기", + "ut_mt": "업로드 중 다른 파일 해싱 계속하기$N$NCPU 또는 HDD가 병목 현상을 일으키는 경우 비활성화하세요", + "ut_ask": '업로드 시작 전 확인 요청">💭', + "ut_pot": "느린 기기에서 UI를 단순화하여$N업로드 속도 향상", + "ut_srch": "실제로 업로드하는 대신, 파일이 이미 서버에 있는지 확인합니다$N(읽을 수 있는 모든 폴더를 스캔합니다)", + "ut_par": "0으로 설정하여 업로드 일시정지$N$N연결이 느리거나 지연 시간이 길면 늘리세요$N$NLAN 환경이거나 서버 HDD가 병목 현상을 일으키면 1로 유지하세요", + "ul_btn": "파일/폴더를 여기에<br>끌어다 놓거나 클릭하세요", + "ul_btnu": "업 로 드", + "ul_btns": "검 색", + + "ul_hash": "해싱", + "ul_send": "전송", + "ul_done": "완료", + "ul_idle1": "대기 중인 업로드가 없습니다", + "ut_etah": "평균 <em>해싱</em> 속도 및 예상 완료 시간", + "ut_etau": "평균 <em>업로드</em> 속도 및 예상 완료 시간", + "ut_etat": "평균 <em>총</em> 속도 및 예상 완료 시간", + + "uct_ok": "성공적으로 완료됨", + "uct_ng": "문제 발생: 실패/거부/찾을 수 없음", + "uct_done": "완료됨 (성공 및 문제 발생 포함)", + "uct_bz": "해싱 또는 업로드 중", + "uct_q": "대기 중, 보류 중", + + "utl_name": "파일명", + "utl_ulist": "목록", + "utl_ucopy": "복사", + "utl_links": "링크", + "utl_stat": "상태", + "utl_prog": "진행률", + + // keep short: + "utl_404": "404", + "utl_err": "오류", + "utl_oserr": "OS 오류", + "utl_found": "찾음", + "utl_defer": "보류", + "utl_yolo": "YOLO", + "utl_done": "완료", + + "ul_flagblk": "파일이 대기열에 추가되었습니다.</b><br>하지만 다른 브라우저 탭에서 up2k가 실행 중이므로,<br>해당 작업이 끝날 때까지 기다립니다.", + "ul_btnlk": "서버 구성에서 이 스위치를 현재 상태로 잠갔습니다.", + + "udt_up": "업로드", + "udt_srch": "검색", + "udt_drop": "여기에 놓으세요", + + "u_nav_m": '<h6>자, 갖고 있는 게 무엇인가?</h6><code>Enter</code> = 파일 (하나 이상)\n<code>ESC</code> = 폴더 하나 (하위 폴더 포함)', + "u_nav_b": '<a href="#" id="modal-ok">파일</a><a href="#" id="modal-ng">폴더 하나</a>', + + "cl_opts": "스위치", + "cl_themes": "테마", + "cl_langs": "언어", + "cl_ziptype": "폴더 다운로드", + "cl_uopts": "up2k 스위치", + "cl_favico": "파비콘", + "cl_bigdir": "큰 디렉터리", + "cl_hsort": "#sort", + "cl_keytype": "조성 표기법", + "cl_hiddenc": "숨겨진 열", + "cl_hidec": "숨기기", + "cl_reset": "초기화", + "cl_hpick": "아래 테이블에서 숨기고 싶은 열의 헤더를 탭하세요", + "cl_hcancel": "열 숨기기가 중단되었습니다", + + "ct_grid": "田 그리드", + "ct_ttips": '◔ ◡ ◔">ℹ️ 도움말', + "ct_thumb": '그리드 보기에서 아이콘 또는 미리보기 이미지 전환$N단축키: T">🖼️ 미리보기', + "ct_csel": '그리드 보기에서 CTRL과 SHIFT를 사용하여 파일 선택">선택', + "ct_ihop": '이미지 뷰어를 닫으면 마지막으로 본 파일로 스크롤">g⮯', + "ct_dots": '숨김 파일 표시 (서버가 허용하는 경우)">숨김파일', + "ct_qdel": '파일 삭제 시 한 번만 확인 요청">빠른삭제', + "ct_dir1st": '폴더를 파일보다 먼저 정렬">📁 먼저', + "ct_nsort": '자연어 정렬 (파일명의 숫자를 인식)">자연어정렬', + "ct_utc": '모든 날짜/시간을 UTC로 표시">UTC', + "ct_readme": '폴더 목록에 README.md 표시">📜 readme', + "ct_idxh": '폴더 목록 대신 index.html 표시">htm', + "ct_sbars": '스크롤바 표시">⟊', + + "cut_umod": '파일이 서버에 이미 있는 경우, 서버의 마지막 수정 타임스탬프를 로컬 파일과 일치하도록 업데이트합니다 (쓰기+삭제 권한 필요).\">re📅', + + "cut_turbo": 'YOLO 버튼. 아마 활성화하고 싶지 않으실 겁니다.$N$N대량의 파일을 업로드하다가 어떤 이유로 재시작해야 할 때, 최대한 빨리 업로드를 계속하고 싶을 때 사용하세요.$N$N이 옵션은 해시 확인을 단순히 <em>"서버에 동일한 파일 크기를 가진 파일이 있는가?"</em>로 대체하므로, 파일 내용만 다를 경우 업로드되지 않습니다.$N$N업로드가 끝나면 이 옵션을 끄고, 동일한 파일을 다시 \"업로드\"하여 클라이언트가 검증하도록 해야 합니다.\">turbo', + + "cut_datechk": '터보 버튼이 활성화되어 있지 않으면 효과가 없습니다.$N$NYOLO의 위험성을 약간 줄여줍니다. 서버의 파일 타임스탬프가 사용자의 것과 일치하는지 확인합니다.$N$N<em>이론적으로는</em> 대부분의 미완료/손상된 업로드를 잡아내지만, 터보를 비활성화하고 검증 과정을 거치는 것을 대체할 수는 없습니다.\">날짜확인', + + "cut_u2sz": "각 업로드 청크의 크기 (MiB)입니다. 큰 값은 태평양을 건너는 데 더 유리합니다. 매우 불안정한 연결에서는 낮은 값을 시도해보세요.", + + "cut_flag": '한 번에 하나의 탭만 업로드하도록 보장합니다.$N-- 다른 탭도 이 옵션을 활성화해야 합니다.$N-- 동일한 도메인의 탭에만 영향을 미칩니다.', + + "cut_az": '가장 작은 파일 우선이 아닌 알파벳 순서로 파일을 업로드합니다.$N$N알파벳 순서는 서버에서 문제가 발생했는지 눈으로 확인하기 쉽게 해주지만, 광랜/LAN 환경에서는 업로드 속도가 약간 느려집니다.', + + "cut_nag": '업로드 완료 시 OS 알림$N(브라우저나 탭이 활성화되지 않은 경우에만)', + "cut_sfx": '업로드 완료 시 소리 알림$N(브라우저나 탭이 활성화되지 않은 경우에만)', + + "cut_mt": '멀티스레딩을 사용하여 파일 해싱 속도를 높입니다.$N$N이 기능은 웹 워커를 사용하며$N더 많은 RAM이 필요합니다 (추가적으로 최대 512 MiB).$N$Nhttps는 30% 더 빠르게, http는 4.5배 더 빠르게 만듭니다.\">mt', + + "cut_wasm": '브라우저 내장 해셔 대신 wasm을 사용합니다. 크롬 기반 브라우저에서 속도를 향상시키지만 CPU 부하를 증가시키며, 많은 구버전 크롬에는 이 기능을 활성화하면 모든 RAM을 소모하고 충돌하는 버그가 있습니다.\">wasm', + + "cft_text": "파비콘 텍스트 (비워두고 새로고침하면 비활성화됨)", + "cft_fg": "전경색", + "cft_bg": "배경색", + + "cdt_lim": "폴더에 표시할 최대 파일 수", + "cdt_ask": "맨 아래로 스크롤할 때$N더 많은 파일을 불러오는 대신$N무엇을 할지 묻기", + "cdt_hsort": "미디어 URL에 포함할 정렬 규칙 (<code>,sorthref</code>)의 수. 0으로 설정하면 미디어 링크를 클릭할 때 포함된 정렬 규칙도 무시됩니다.", + + "tt_entree": "탐색 창 (디렉터리 트리 사이드바) 표시$N단축키: B", + "tt_detree": "이동 경로 표시$N단축키: B", + "tt_visdir": "선택한 폴더로 스크롤하기", + "tt_ftree": "폴더 트리/텍스트 파일 전환$N단축키: V", + "tt_pdock": "상위 폴더를 상단에 고정된 창에 표시", + "tt_dynt": "트리가 확장될 때 자동으로 너비 증가", + "tt_wrap": "자동 줄 바꿈", + "tt_hover": "마우스를 올리면 넘어가는 줄 표시$N(마우스 커서가 왼쪽 여백에$N  있지 않으면 스크롤이 깨짐)", + + "ml_pmode": "폴더 끝에서...", + "ml_btns": "명령", + "ml_tcode": "트랜스코딩", + "ml_tcode2": "다음으로 트랜스코딩", + "ml_tint": "틴트", + "ml_eq": "오디오 이퀄라이저", + "ml_drc": "다이내믹 레인지 압축기", + + "mt_loop": "한 곡 반복 재생\">🔁", + "mt_one": "한 곡 재생 후 중지\">1️⃣", + "mt_shuf": "각 폴더의 곡을 무작위 재생\">🔀", + "mt_aplay": "서버에 접속한 링크에 곡 ID가 있으면 자동 재생$N$N이것을 비활성화하면 음악 재생 시 페이지 URL이 곡 ID로 업데이트되지 않아, 이 설정이 손실되고 URL이 남아있을 경우 자동 재생되는 것을 방지합니다.\">a▶", + "mt_preload": "끊김 없는 재생을 위해 다음 곡을 미리 불러오기 시작\">미리로드", + "mt_prescan": "마지막 곡이 끝나기 전에 다음 폴더로 이동하여$N웹브라우저가 재생을 멈추지 않도록 합니다.\">탐색", + "mt_fullpre": "전체 곡을 미리 불러오기 시도;$N✅ <b>불안정한</b> 연결에서 활성화,$N❌ <b>느린</b> 연결에서는 아마도 비활성화\">전체", + "mt_fau": "폰에서 다음 곡이 충분히 빨리 미리 불러오지 않아 음악이 멈추는 것을 방지합니다 (태그 표시가 불안정해질 수 있음).\">☕️", + "mt_waves": "파형 탐색 바:$N탐색 바에 오디오 진폭 표시\">~s", + "mt_npclip": "현재 재생 중인 곡을 클립보드에 복사하는 버튼 표시\">/np", + "mt_m3u_c": "선택한 곡을 m3u8 재생 목록 항목으로$N클립보드에 복사하는 버튼 표시\">📻", + "mt_octl": "OS 통합 (미디어 단축키/OSD)\">os-ctl", + "mt_oseek": "OS 통합을 통해 탐색 허용$N$N참고: 일부 기기 (iPhone)에서는$N이것이 다음 곡 버튼을 대체합니다.\">탐색", + "mt_oscv": "OSD에 앨범 커버 표시\">아트", + "mt_follow": "재생 중인 트랙이 보이도록 스크롤 유지\">🎯", + "mt_compact": "컴팩트 컨트롤\">⟎", + "mt_uncache": "캐시 지우기 (브라우저가 곡의 깨진 사본을 캐시하여$N재생이 안되는 경우 시도해보세요)\">캐시삭제", + "mt_mloop": "열린 폴더 반복\">🔁 반복", + "mt_mnext": "다음 폴더 불러오고 계속\">📂 다음", + "mt_mstop": "재생 중지\">⏸ 중지", + "mt_cflac": "flac/wav를 opus로 변환\">flac", + "mt_caac": "aac/m4a를 opus로 변환\">aac", + "mt_coth": "다른 모든 것 (mp3 제외)을 opus로 변환\">기타", + "mt_c2opus": "데스크톱, 노트북, 안드로이드 환경에 최적\">opus", + "mt_c2owa": "iOS 17.5 이상용 opus-weba\">owa", + "mt_c2caf": "iOS 11부터 17까지용 opus-caf\">caf", + "mt_c2mp3": "매우 오래된 기기에서 사용\">mp3", + "mt_c2flac": "최고 음질이지만 다운로드 용량이 큼\">flac", + "mt_c2wav": "비압축 재생 (더 큼)\">wav", + "mt_c2ok": "네, 좋은 선택입니다", + "mt_c2nd": "기기에 권장되는 출력 형식이 아니지만 괜찮습니다", + "mt_c2ng": "기기가 이 출력 형식을 지원하지 않는 것 같지만, 시도해 보겠습니다", + "mt_xowa": "iOS에서 이 형식의 백그라운드 재생이 안되는 버그가 있습니다. 대신 caf나 mp3를 사용해주세요.", + "mt_tint": "탐색 바의 배경 레벨 (0-100)$N버퍼링이 덜 눈시리게 만듦", + "mt_eq": "이퀄라이저 및 게인 제어 활성화;$N$Nboost <code>0</code> = 표준 100% 볼륨 (수정 없음)$N$Nwidth <code>1  </code> = 표준 스테레오 (수정 없음)$Nwidth <code>0.5</code> = 50% 좌우 크로스피드$Nwidth <code>0  </code> = 모노$N$Nboost <code>-0.8</code> & width <code>10</code> = 보컬 제거 :^)$N$N이퀄라이저를 활성화하면 끊김 없는 앨범이 온전히 끊김 없이 재생되므로, 그 점이 중요하다면 모든 값을 0으로 두고 (width=1 제외) 켜두세요.", + "mt_drc": "다이내믹 레인지 컴프레서(볼륨 평탄화/벽돌화)를 활성화합니다. 스파게티의 균형을 맞추기 위해 EQ도 활성화되므로, 원하지 않으면 'width'를 제외한 모든 EQ 필드를 0으로 설정하세요.$N$NTHRESHOLD dB 이상의 오디오 볼륨을 낮춥니다. THRESHOLD를 초과하는 모든 RATIO dB에 대해 1dB의 출력이 있으므로, 기본값인 tresh -24 및 ratio 12는 볼륨이 -22dB보다 커지지 않음을 의미하며, 이퀄라이저 부스트를 0.8 또는 ATK 0과 큰 RLS (예: 90)를 사용하여 1.8까지 안전하게 높일 수 있습니다 (firefox에서만 작동, 다른 브라우저에서는 RLS 최대 1).$N$N(위키백과를 참조하세요, 훨씬 더 잘 설명되어 있습니다)", + + "mb_play": "재생", + "mm_hashplay": "이 오디오 파일을 재생할까요?", + "mm_m3u": "<code>Enter/확인</code>을 눌러 재생\n<code>ESC/취소</code>를 눌러 편집", + "mp_breq": "Firefox 82+, Chrome 73+ 또는 iOS 15+ 필요", + "mm_bload": "불러오는 중...", + "mm_bconv": "{0}(으)로 변환 중, 잠시만 기다려주세요...", + "mm_opusen": "브라우저가 aac/m4a 파일을 재생할 수 없습니다.\nopus로의 트랜스코딩이 활성화되었습니다.", + "mm_playerr": "재생 실패: ", + "mm_eabrt": "재생 시도가 취소되었습니다", + "mm_enet": "인터넷 연결이 불안정합니다", + "mm_edec": "이 파일이 손상된 것 같습니다??", + "mm_esupp": "브라우저가 이 오디오 형식을 이해하지 못합니다", + "mm_eunk": "알 수 없는 오류", + "mm_e404": "오디오를 재생할 수 없습니다; 오류 404: 파일을 찾을 수 없습니다.", + "mm_e403": "오디오를 재생할 수 없습니다; 오류 403: 접근이 거부되었습니다.\n\nF5를 눌러 새로고침 해보세요, 로그아웃되었을 수 있습니다", + "mm_e500": "오디오를 재생할 수 없습니다; 오류 500: 서버 로그를 확인하세요.", + "mm_e5xx": "오디오를 재생할 수 없습니다; 서버 오류 ", + "mm_nof": "주변에서 더 이상 오디오 파일을 찾을 수 없습니다", + "mm_prescan": "다음에 재생할 음악을 찾는 중...", + "mm_scank": "다음 곡을 찾았습니다:", + "mm_uncache": "캐시가 지워졌습니다. 모든 곡은 다음 재생 시 다시 다운로드됩니다.", + "mm_hnf": "그 곡이 더 이상 존재하지 않습니다", + + "im_hnf": "그 이미지가 더 이상 존재하지 않습니다", + + "f_empty": '이 폴더는 비어 있습니다', + "f_chide": '«{0}» 열을 숨깁니다.\n\n설정 탭에서 열을 다시 표시할 수 있습니다.', + "f_bigtxt": "이 파일은 {0} MiB입니다 -- 정말 텍스트로 보시겠습니까?", + "f_bigtxt2": "대신 파일의 끝부분만 보시겠습니까? 이렇게 하면 실시간으로 새로 추가되는 텍스트 줄을 보여주는 팔로잉/테일링 기능도 활성화됩니다.", + "fbd_more": '<div id="blazy"><code>{1}</code>개 파일 중 <code>{0}</code>개 표시 중; <a href="#" id="bd_more">{2}개 더 보기</a> 또는 <a href="#" id="bd_all">모두 보기</a></div>', + "fbd_all": '<div id="blazy"><code>{1}</code>개 파일 중 <code>{0}</code>개 표시 중; <a href="#" id="bd_all">모두 보기</a></div>', + "f_anota": "{1}개 항목 중 {0}개만 선택되었습니다.\n전체 폴더를 선택하려면 먼저 맨 아래로 스크롤하세요.", + + "f_dls": '현재 폴더의 파일 링크가\n다운로드 링크로 변경되었습니다', + + "f_partial": "현재 업로드 중인 파일을 안전하게 다운로드하려면, 파일 이름이 같지만 <code>.PARTIAL</code> 확장자가 없는 파일을 클릭하세요. 이 경고를 무시하려면 \"취소\" 또는 ESC를 누르세요.\n\n\"확인\"/Enter를 누르면 이 경고를 무시하고 <code>.PARTIAL</code> 임시 파일을 계속 다운로드하며, 이 경우 거의 확실히 손상된 데이터를 받게 됩니다.", + + "ft_paste": "{0}개 항목 붙여넣기$N단축키: ctrl-V", + "fr_eperm": "이름을 바꿀 수 없습니다:\n이 폴더에 \"이동\" 권한이 없습니다", + "fd_eperm": "삭제할 수 없습니다:\n이 폴더에 \"삭제\" 권한이 없습니다", + "fc_eperm": "잘라낼 수 없습니다:\n이 폴더에 \"이동\" 권한이 없습니다", + "fp_eperm": "붙여넣을 수 없습니다\n이 폴더에 \"쓰기\" 권한이 없습니다", + "fr_emore": "이름을 바꿀 항목을 하나 이상 선택하세요", + "fd_emore": "삭제할 항목을 하나 이상 선택하세요", + "fc_emore": "잘라낼 항목을 하나 이상 선택하세요", + "fcp_emore": "클립보드에 복사할 항목을 하나 이상 선택하세요", + + "fs_sc": "현재 폴더 공유", + "fs_ss": "선택한 파일 공유", + "fs_just1d": "하나 이상의 폴더를 선택하거나,\n파일과 폴더를 한 번에 섞어 선택할 수 없습니다", + "fs_abrt": "❌ 중단", + "fs_rand": "🎲 무작위 이름", + "fs_go": "✅ 공유 생성", + "fs_name": "이름", + "fs_src": "소스", + "fs_pwd": "비밀번호", + "fs_exp": "만료", + "fs_tmin": "분", + "fs_thrs": "시간", + "fs_tdays": "일", + "fs_never": "영원", + "fs_pname": "선택적 링크 이름; 비워두면 무작위로 생성", + "fs_tsrc": "공유할 파일 또는 폴더", + "fs_ppwd": "비밀번호 (선택사항)", + "fs_w8": "공유 생성 중...", + "fs_ok": "<code>Enter/OK</code>를 눌러 클립보드에 복사\n<code>ESC/Cancel</code>를 눌러 닫기", + + "frt_dec": "깨진 파일 이름의 일부 경우를 수정할 수 있습니다\">url-디코드", + "frt_rst": "수정된 파일 이름을 원래대로 되돌립니다\">↺ 초기화", + "frt_abrt": "이 창을 중단하고 닫습니다\">❌ 취소", + "frb_apply": "이름 바꾸기 적용", + "fr_adv": "배치/메타데이터/패턴 이름 바꾸기\">고급", + "fr_case": "대소문자 구분 정규식\">대소문자", + "fr_win": "Windows 안전 이름; <code><>:"\\|?*</code>를 일본어 전각 문자로 바꿉니다\">win", + "fr_slash": "<code>/</code>를 새 폴더를 만들지 않는 문자로 바꿉니다\">/ 없음", + "fr_re": "원본 파일 이름에 적용할 정규식 검색 패턴; 캡처링 그룹은 아래 형식 필드에서 <code>(1)</code>, <code>(2)</code> 등으로 참조할 수 있습니다", + "fr_fmt": "foobar2000에서 영감을 받음:$N<code>(title)</code>은(는) 곡 제목으로 대체됨,$N<code>[(artist) - ](title)</code>은(는) 아티스트가 비어 있으면 [이] 부분을 건너뜀$N<code>$lpad((tn),2,0)</code>은(는) 트랙 번호를 2자리로 채움", + "fr_pdel": "삭제", + "fr_pnew": "다른 이름으로 저장", + "fr_pname": "새 프리셋의 이름을 입력하세요", + "fr_aborted": "중단됨", + "fr_lold": "이전 이름", + "fr_lnew": "새 이름", + "fr_tags": "선택한 파일의 태그 (읽기 전용, 참조용):", + "fr_busy": "{0}개 항목 이름 바꾸는 중...\n\n{1}", + "fr_efail": "이름 바꾸기 실패:\n", + "fr_nchg": "<code>win</code> 및/또는 <code>/ 없음</code>으로 인해 새 이름 중 {0}개가 변경되었습니다.\n\n이 변경된 새 이름으로 계속하시겠습니까?", + + "fd_ok": "삭제 확인", + "fd_err": "삭제 실패:\n", + "fd_none": "아무것도 삭제되지 않았습니다. 서버 구성 (xbd)에 의해 차단되었을 수 있습니다.", + "fd_busy": "삭제 중 {0}개 항목...\n\n{1}", + "fd_warn1": "이 {0}개 항목을 삭제하시겠습니까?", + "fd_warn2": "<b>마지막 기회입니다!</b> 되돌릴 수 없습니다. 삭제하시겠습니까?", + + "fc_ok": "{0}개 항목 잘라내기 완료", + "fc_warn": "{0}개 항목 잘라내기 완료\n\n하지만: 선택 항목이 너무 커서 <b>이</b> 브라우저 탭에서만 붙여넣을 수 있습니다", + + "fcc_ok": "{0}개 항목을 클립보드에 복사했습니다", + "fcc_warn": "{0}개 항목을 클립보드에 복사했습니다\n\n하지만: 선택 항목이 너무 커서 <b>이</b> 브라우저 탭에서만 붙여넣을 수 있습니다", + + "fp_apply": "이 이름 사용", + "fp_ecut": "붙여넣거나 이동하려면 먼저 파일/폴더를 잘라내거나 복사하세요\n\n참고: 다른 브라우저 탭 간에 잘라내기/붙여넣기를 할 수 있습니다", + "fp_ename": "이름이 이미 사용 중이므로 {0}개 항목을 여기로 이동할 수 없습니다. 계속하려면 아래에 새 이름을 지정하거나, 이름을 비워두면 건너뜁니다:", + "fcp_ename": "이름이 이미 사용 중이므로 {0}개 항목을 여기로 복사할 수 없습니다. 계속하려면 아래에 새 이름을 지정하거나, 이름을 비워두면 건너뜁니다:", + "fp_emore": "아직 해결해야 할 파일 이름 충돌이 남아 있습니다", + "fp_ok": "이동 완료", + "fcp_ok": "복사 완료", + "fp_busy": "{0}개 항목 이동 중...\n\n{1}", + "fcp_busy": "{0}개 항목 복사 중...\n\n{1}", + "fp_err": "이동 실패:\n", + "fcp_err": "복사 실패:\n", + "fp_confirm": "이 {0}개 항목을 여기로 이동하시겠습니까?", + "fcp_confirm": "이 {0}개 항목을 여기로 복사하시겠습니까?", + "fp_etab": '다른 브라우저 탭에서 클립보드를 읽지 못했습니다', + "fp_name": "기기에서 파일을 업로드합니다. 이름을 지정하세요:", + "fp_both_m": '<h6>붙여넣을 항목 선택</h6><code>Enter</code> = «{1}»에서 파일 {0}개 이동\n<code>ESC</code> = 기기에서 파일 {2}개 업로드', + "fcp_both_m": '<h6>붙여넣을 항목 선택</h6><code>Enter</code> = «{1}»에서 파일 {0}개 복사\n<code>ESC</code> = 기기에서 파일 {2}개 업로드', + "fp_both_b": '<a href="#" id="modal-ok">이동</a><a href="#" id="modal-ng">업로드</a>', + "fcp_both_b": '<a href="#" id="modal-ok">복사</a><a href="#" id="modal-ng">업로드</a>', + + "mk_noname": "왼쪽 텍스트 필드에 이름을 먼저 입력해주세요 :p", + + "tv_load": "텍스트 문서 불러오는 중:\n\n{0}\n\n{1}% ({3} MiB 중 {2} MiB 로드됨)", + "tv_xe1": "텍스트 파일을 불러올 수 없습니다:\n\n오류 ", + "tv_xe2": "404, 파일을 찾을 수 없음", + "tv_lst": "텍스트 파일 목록", + "tvt_close": "폴더 보기로 돌아가기$N단축키: M (또는 Esc)\">❌ 닫기", + "tvt_dl": "이 파일 다운로드$N단축키: Y\">💾 다운로드", + "tvt_prev": "이전 문서 보기$N단축키: i\">⬆ 이전", + "tvt_next": "다음 문서 보기$N단축키: K\">⬇ 다음", + "tvt_sel": "파일 선택   (잘라내기/복사/삭제/...용)$N단축키: S\">선택", + "tvt_edit": "텍스트 편집기에서 파일 열기$N단축키: E\">✏️ 편집", + "tvt_tail": "파일 변경 사항 모니터링; 실시간으로 새 줄 표시\">📡 팔로우", + "tvt_wrap": "자동 줄 바꿈\">↵", + "tvt_atail": "페이지 하단으로 스크롤 고정\">⚓", + "tvt_ctail": "터미널 색상 디코딩 (ANSI 이스케이프 코드)\">🌈", + "tvt_ntail": "스크롤백 제한 (불러온 상태로 유지할 텍스트 바이트 수)", + + "m3u_add1": "m3u 재생 목록에 곡이 추가되었습니다", + "m3u_addn": "{0}개의 곡이 m3u 재생 목록에 추가되었습니다", + "m3u_clip": "m3u 재생 목록이 클립보드에 복사되었습니다\n\n something.m3u와 같은 이름의 새 텍스트 파일을 만들고 그 문서에 재생 목록을 붙여넣으면 재생할 수 있습니다.", + + "gt_vau": "비디오를 표시하지 않고 오디오만 재생\">🎧", + "gt_msel": "파일 선택 활성화; ctrl-클릭하여 파일 재정의$N$N<em>활성 시: 파일/폴더를 두 번 클릭하여 열기</em>$N$N단축키: S\">다중선택", + "gt_crop": "썸네일 중앙 자르기\">자르기", + "gt_3x": "고해상도 썸네일\">3x", + "gt_zoom": "확대/축소", + "gt_chop": "자르기", + "gt_sort": "정렬 기준", + "gt_name": "이름", + "gt_sz": "크기", + "gt_ts": "날짜", + "gt_ext": "유형", + "gt_c1": "파일명 더 많이 생략하기 (더 적게 표시)", + "gt_c2": "파일명 덜 생략하기 (더 많이 표시)", + + "sm_w8": "검색 중...", + "sm_prev": "아래 검색 결과는 이전 검색어에 대한 결과입니다:\n  ", + "sl_close": "검색 결과 닫기", + "sl_hits": "{0}개 결과 표시 중", + "sl_moar": "더 불러오기", + + "s_sz": "크기", + "s_dt": "날짜", + "s_rd": "경로", + "s_fn": "이름", + "s_ta": "태그", + "s_ua": "업로드 시점", + "s_ad": "고급", + "s_s1": "최소 MiB", + "s_s2": "최대 MiB", + "s_d1": "최소 ISO-8601", + "s_d2": "최대 ISO-8601", + "s_u1": "이후", + "s_u2": "이전", + "s_r1": "경로에 포함   (공백으로 구분)", + "s_f1": "이름에 포함   (-로 제외)", + "s_t1": "태그에 포함   (^=시작, 끝=$)", + "s_a1": "특정 메타데이터 속성", + + "md_eshow": "렌더링할 수 없음 ", + "md_off": "[📜<em>readme</em>]가 [⚙️]에서 비활성화됨 -- 문서 숨김", + + "badreply": "서버로부터의 응답을 구문 분석하지 못했습니다", + + "xhr403": "403: 접근 거부됨\n\nF5를 눌러보세요, 로그아웃되었을 수 있습니다", + "xhr0": "알 수 없음 (서버와의 연결이 끊겼거나 서버가 오프라인일 수 있습니다)", + "cf_ok": "죄송합니다 -- DD" + wah + "oS 보호 기능이 작동했습니다\n\n약 30초 후에 다시 정상적으로 작동할 것입니다\n\n아무 일도 일어나지 않으면 F5를 눌러 페이지를 새로고침하세요", + "tl_xe1": "하위 폴더를 나열할 수 없습니다:\n\n오류 ", + "tl_xe2": "404: 폴더를 찾을 수 없음", + "fl_xe1": "폴더의 파일을 나열할 수 없습니다:\n\n오류 ", + "fl_xe2": "404: 폴더를 찾을 수 없음", + "fd_xe1": "하위 폴더를 만들 수 없습니다:\n\n오류 ", + "fd_xe2": "404: 상위 폴더를 찾을 수 없음", + "fsm_xe1": "메시지를 보낼 수 없습니다:\n\n오류 ", + "fsm_xe2": "404: 상위 폴더를 찾을 수 없음", + "fu_xe1": "서버에서 주워담기 목록을 불러오지 못했습니다:\n\n오류 ", + "fu_xe2": "404: 파일을 찾을 수 없음??", + + "fz_tar": "압축되지 않은 gnu-tar 파일 (linux / mac)", + "fz_pax": "압축되지 않은 pax 형식 tar (느림)", + "fz_targz": "gzip 레벨 3 압축이 적용된 gnu-tar$N$N이것은 보통 매우 느리므로$N압축되지 않은 tar를 대신 사용하세요", + "fz_tarxz": "xz 레벨 1 압축이 적용된 gnu-tar$N$N이것은 보통 매우 느리므로$N압축되지 않은 tar를 대신 사용하세요", + "fz_zip8": "utf8 파일 이름이 포함된 zip (windows 7 및 이전 버전에서 문제가 있을 수 있음)", + "fz_zipd": "정말 오래된 소프트웨어를 위한 전통적인 cp437 파일 이름이 포함된 zip", + "fz_zipc": "MS-DOS PKZIP v2.04g (1993년 10월)용으로$Ncrc32가 미리 계산된 cp437$N(다운로드 시작 전 처리 시간이 더 걸림)", + + "un_m1": "아래에서 최근 업로드를 삭제하거나 미완료된 업로드를 중단할 수 있습니다", + "un_upd": "새로고침", + "un_m4": "또는 아래에 보이는 파일을 공유할 수 있습니다:", + "un_ulist": "보기", + "un_ucopy": "복사", + "un_flt": "선택적 필터:  URL에 포함되어야 함", + "un_fclr": "필터 지우기", + "un_derr": '주워담기-삭제 실패:\n', + "un_f5": '문제가 발생했습니다, 새로고침하거나 F5를 눌러보세요', + "un_uf5": "죄송하지만, 이 업로드를 중단하기 전에 페이지를 새로고침해야 합니다 (예: F5 또는 CTRL-R 누르기).", + "un_nou": '<b>경고:</b> 서버가 너무 바빠서 미완료 업로드를 표시할 수 없습니다; 잠시 후 "새로고침" 링크를 클릭하세요', + "un_noc": '<b>경고:</b> 완전히 업로드된 파일의 주워담기가 서버 구성에서 활성화/허용되지 않았습니다', + "un_max": "처음 2000개 파일을 표시합니다 (필터를 사용하세요)", + "un_avail": "{0}개의 최근 업로드를 삭제할 수 있습니다<br />{1}개의 미완료 업로드를 중단할 수 있습니다", + "un_m2": "업로드 시간순으로 정렬됨. 가장 최근 항목이 먼저 표시:", + "un_no1": "아쉽다! 충분히 최근인 업로드가 없습니다.", + "un_no2": "아쉽다! 해당 필터와 일치하는 최근 업로드가 없습니다.", + "un_next": "아래의 다음 {0}개 파일 삭제", + "un_abrt": "중단", + "un_del": "삭제", + "un_m3": "최근 업로드 로드 중...", + "un_busy": "{0}개 파일 삭제 중...", + "un_clip": "{0}개의 링크가 클립보드에 복사되었습니다", + + "u_https1": "더 나은 성능을 위해", + "u_https2": "https로 전환", + "u_https3": "하는 것이 좋습니다", + "u_ancient": '브라우저가 정말 오래되었네요 -- 아마도 <a href="#" onclick="goto(\'bup\')">bup을 대신 사용</a>해야 할 것 같습니다', + "u_nowork": "Firefox 53+, Chrome 57+ 또는 iOS 11+가 필요합니다", + "tail_2old": "Firefox 105+, Chrome 71+ 또는 iOS 14.5+가 필요합니다", + "u_nodrop": '브라우저가 너무 오래되어 드래그 앤 드롭 업로드를 지원하지 않습니다', + "u_notdir": '폴더가 아닙니다!\n\n브라우저가 너무 오래되었습니다,\n대신 드래그드롭을 시도해보세요', + "u_uri": '다른 브라우저 창에서 이미지를 드래그드롭하려면,\n큰 업로드 버튼 위로 떨어뜨려주세요', + "u_enpot": '<a href="#">단순 UI로 전환</a> (업로드 속도가 향상될 수 있음)', + "u_depot": '<a href="#">화려한 UI로 전환</a> (업로드 속도가 감소할 수 있음)', + "u_gotpot": '업로드 속도 향상을 위해 단순 UI로 전환합니다,\n\n언제든지 다시 전환하셔도 좋습니다!', + "u_pott": "<p>파일:   <b>{0}</b> 완료,   <b>{1}</b> 실패,   <b>{2}</b> 처리 중,   <b>{3}</b> 대기 중</p>", + "u_ever": "이것은 기본 업로더입니다. up2k는 최소한 다음 버전이 필요합니다:<br>Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1", + "u_su2k": '이것은 기본 업로더입니다. <a href="#" id="u2yea">up2k</a>가 더 좋습니다', + "u_uput": '속도 최적화 (체크섬 건너뛰기)', + "u_ewrite": '이 폴더에 쓰기 권한이 없습니다', + "u_eread": '이 폴더에 읽기 권한이 없습니다', + "u_enoi": '파일 검색이 서버 구성에서 활성화되지 않았습니다', + "u_enoow": '여기서는 덮어쓰기가 작동하지 않습니다. 삭제 권한이 필요합니다', + "u_badf": '총 {1}개 중 다음 {0}개의 파일은 파일 시스템 권한 문제 등으로 건너뛰었습니다:\n\n', + "u_blankf": '총 {1}개 중 다음 {0}개의 파일은 비어있습니다. 그래도 업로드하시겠습니까?\n\n', + "u_applef": '총 {1}개 중 다음 {0}개의 파일은 아마도 불필요한 파일일 것입니다.\n다음 파일을 건너뛰려면 <code>확인/Enter</code>를 누르세요,\n해당 파일도 업로드하려면 <code>취소/ESC</code>를 누르세요:\n\n', + "u_just1": '\n파일을 하나만 선택하면 더 잘 작동할 수 있습니다', + "u_ff_many": '<b>리눅스/macOS/안드로이드</b>를 사용 중이라면, 이 정도의 파일 수는 <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1790500" target="_blank">Firefox를 <em>충돌시킬 수 있습니다!</em></a>\n만약 그런 일이 발생하면 다시 시도하거나 Chrome을 사용해주세요.', + "u_up_life": '이 업로드는 완료 후 {0} 뒤에\n서버에서 삭제됩니다', + "u_asku": '이 {0}개의 파일을 <code>{1}</code>(으)로 업로드하시겠습니까?', + "u_unpt": '왼쪽 상단의 🧯를 사용하여 이 업로드를 취소/삭제할 수 있습니다', + "u_bigtab": '{0}개의 파일을 표시하려고 합니다\n\n브라우저가 충돌할 수 있습니다, 계속 진행합니까?', + "u_scan": '파일 스캔 중...', + "u_dirstuck": '디렉터리 반복자가 다음 {0}개 항목에 접근하는 데 실패하여 건너뜁니다:', + "u_etadone": '완료 ({0}, {1}개 파일)', + "u_etaprep": '(업로드 준비 중)', + "u_hashdone": '해싱 완료', + "u_hashing": '해시', + "u_hs": '핸드셰이킹 중...', + "u_started": '파일이 현재 업로드 중입니다. [🚀] 참조', + "u_dupdefer": '중복됨. 다른 모든 파일 처리 후 처리됩니다', + "u_actx": "다른 창/탭으로 전환 시 성능 저하를<br />방지하려면 이 텍스트를 클릭하세요", + "u_fixed": "OK!  해결됐습니다 👍", + "u_cuerr": "{1} 중 청크 {0} 업로드 실패;\n아마 문제 없을 겁니다. 계속 진행합니다\n\n파일: {2}", + "u_cuerr2": "서버가 업로드를 거부했습니다 (청크 {0}/{1});\n나중에 다시 시도합니다\n\n파일: {2}\n\n오류 ", + "u_ehstmp": "다시 시도합니다; 오른쪽 하단 참조", + "u_ehsfin": "서버가 업로드 완료 요청을 거부했습니다. 재시도 중...", + "u_ehssrch": "서버가 검색 수행 요청을 거부했습니다. 재시도 중...", + "u_ehsinit": "서버가 업로드 시작 요청을 거부했습니다. 재시도 중...", + "u_eneths": "업로드 핸드셰이크 중 네트워크 오류 발생. 재시도 중...", + "u_enethd": "대상 존재 여부 테스트 중 네트워크 오류 발생; 재시도 중...", + "u_cbusy": '네트워크 문제 후 서버가 다시 우리를 신뢰할 때까지 기다리는 중...', + "u_ehsdf": '서버 디스크 공간이 부족합니다!\n\n누군가 계속할 수 있을 만큼의 공간을\n비워줄 경우를 대비해 계속 재시도합니다', + "u_emtleak1": "웹 브라우저에 메모리 누수가 있는 것 같습니다.\n", + "u_emtleak2": ' <a href="{0}">https로 전환 (권장)</a>하거나 ', + "u_emtleak3": ' ', + "u_emtleakc": '다음을 시도해보세요:\n<ul><li><code>F5</code>를 눌러 페이지를 새로고침하세요</li><li>그런 다음 <code>⚙️ 설정</code>에서  <code>mt</code>  버튼을 비활성화하세요</li><li>그리고 다시 그 업로드를 시도해보세요</li></ul>업로드가 조금 느려지겠지만, 어쩔 수 없죠.\n불편을 드려 죄송합니다!\n\nPS: 이 버그는 Chrome v107에서 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1354816" target="_blank">수정되었습니다</a>.', + "u_emtleakf": '다음을 시도해보세요:\n<ul><li><code>F5</code>를 눌러 페이지를 새로고침하세요</li><li>그런 다음 업로드 UI에서 <code>🥔</code>(단순 UI)를 활성화하세요<li>그리고 다시 그 업로드를 시도해보세요</li></ul>\nPS: Firefox에서 <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1790500" target="_blank">언젠가 이 버그가 수정될 거라 믿습니다</a>.', + "u_s404": '서버에서 찾을 수 없음', + "u_expl": '설명', + "u_maxconn": "대부분의 브라우저는 이를 6으로 제한하지만, Firefox에서는 <code>about:config</code>에서 <code>connections-per-server</code> 설정값으로 높일 수 있습니다.", + "u_tu": '<p class="warn">경고: 터보가 활성화되어 클라이언트가 불완전한 업로드를 감지하고 재개하지 못할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>', + "u_ts": '<p class="warn">경고: 터보가 활성화되어 검색 결과가 부정확할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>', + "u_turbo_c": "터보가 서버 구성에서 비활성화되었습니다", + "u_turbo_g": '이 볼륨 내에서 디렉터리 목록 권한이 없으므로\n터보를 비활성화합니다', + "u_life_cfg": '자동 삭제 시간 <input id="lifem" p="60" /> 분 (또는 <input id="lifeh" p="3600" /> 시간)', + "u_life_est": '업로드가 <span id="lifew" tt="현지 시간">---</span>에 삭제됩니다', + "u_life_max": '이 폴더는 최대 수명을\n{0}(으)로 강제합니다', + "u_unp_ok": '주워담기는 {0} 동안 허용됩니다', + "u_unp_ng": '주워담기는 허용되지 않습니다', + "ue_ro": '이 폴더에 대한 접근은 읽기 전용입니다\n\n', + "ue_nl": '현재 로그인되어 있지 않습니다', + "ue_la": '현재 \'{0}\'(으)로 로그인되어 있습니다', + "ue_sr": '현재 파일 검색 모드입니다\n\n큰 "검색" 버튼 옆의 돋보기 🔎를 클릭하여 업로드 모드로 전환한 후 다시 업로드해보세요\n\n죄송합니다', + "ue_ta": '다시 업로드해보세요, 이제 작동할 겁니다', + "ue_ab": '이 파일은 이미 다른 폴더로 업로드 중이며, 파일이 다른 곳에 업로드되기 전에 해당 업로드가 완료되어야 합니다.\n\n왼쪽 상단의 🧯를 사용하여 초기 업로드를 중단하고 잊을 수 있습니다.', + "ur_1uo": "OK: 파일이 성공적으로 업로드되었습니다", + "ur_auo": "OK: 모든 {0}개의 파일이 성공적으로 업로드되었습니다", + "ur_1so": "OK: 서버에서 파일을 찾았습니다", + "ur_aso": "OK: 서버에서 모든 {0}개의 파일을 찾았습니다", + "ur_1un": "업로드에 실패했습니다, 죄송", + "ur_aun": "모든 {0}개의 업로드에 실패했습니다, 죄송", + "ur_1sn": "서버에서 파일을 찾지 못했습니다", + "ur_asn": "서버에서 {0}개의 파일을 찾지 못했습니다", + "ur_um": "완료;\n{0}개 업로드 성공,\n{1}개 업로드 실패, 죄송", + "ur_sm": "완료;\n서버에서 {0}개 파일 찾음,\n서버에서 {1}개 파일 찾지 못함", + + "lang_set": '변경 사항을 적용하기 위해 새로고침하시겠습니까?' + }, "nld": { "tt": "Nederlands", @@ -10090,7 +10719,7 @@ var Ls = { }, }; -var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "nld", "rus", "spa", "swe", "ukr"]; +var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "kor", "nld", "rus", "spa", "swe", "ukr"]; if (window.langmod) langmod(); diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 3229b9a2..aad6ad2a 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -342,6 +342,48 @@ var Ls = { "af1": "mostra i file caricati di recente", "ag1": "mostra utenti IdP conosciuti" }, + "kor": { + "a1": "새로고침", + "b1": "어이 친구! 처음 보는 얼굴인데?   <small>(로그인되어 있지 않습니다)</small>", + "c1": "로그아웃", + "d1": "스택 덤프하기", + "d2": "모든 활성 스레드의 상태를 표시합니다", + "e1": "설정 다시 불러오기", + "e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다", + "f1": "탐색 가능한 곳:", + "g1": "업로드 가능한 곳:", + "cc1": "기타 항목:", + "h1": "k304 비활성화", + "i1": "k304 활성화", + "j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>", + "k1": "클라이언트 설정 초기화", + "l1": "로그인하기:", + "m1": "또 오셨네요,", + "n1": "404 찾을 수 없음  ┐( ´ -`)┌", + "o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요", + "p1": "403 접근 금지  ~┻━┻", + "q1": "비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요", + "r1": "홈으로 이동", + ".s1": "다시 스캔", + "t1": "작업", + "u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초", + "v1": "연결", + "v2": "이 서버를 로컬 하드디스크처럼 사용하기", + "w1": "HTTPS로 전환", + "x1": "비밀번호 변경", + "y1": "공유 설정", + "z1": "이 공유 잠금해제:", + "ta1": "새 비밀번호를 먼저 입력하세요", + "ta2": "새 비밀번호 확인을 위해 다시 입력하세요:", + "ta3": "오타가 있습니다. 다시 시도해주세요", + "aa1": "수신 중인 파일:", + "ab1": "no304 비활성화", + "ac1": "no304 활성화", + "ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!", + "ae1": "활성 다운로드:", + "af1": "최근 업로드 보기", + "ag1": "IdP 캐시 보기" + }, "nld": { "a1": "Update", "b1": "Hallo, hoe gaat het met jou?   <small>(Je bent niet ingelogd)</small>", From 2961dea5bb46036de9849061b95ef63a24dda217 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 19:36:20 +0000 Subject: [PATCH 038/154] tl cleanup --- copyparty/web/browser.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index a387495e..92bba2ba 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -6102,6 +6102,7 @@ var Ls = { "fcp_ok": "복사 완료", "fp_busy": "{0}개 항목 이동 중...\n\n{1}", "fcp_busy": "{0}개 항목 복사 중...\n\n{1}", + "fp_abrt": "취소 중...", "fp_err": "이동 실패:\n", "fcp_err": "복사 실패:\n", "fp_confirm": "이 {0}개 항목을 여기로 이동하시겠습니까?", @@ -6282,8 +6283,8 @@ var Ls = { "u_s404": '서버에서 찾을 수 없음', "u_expl": '설명', "u_maxconn": "대부분의 브라우저는 이를 6으로 제한하지만, Firefox에서는 <code>about:config</code>에서 <code>connections-per-server</code> 설정값으로 높일 수 있습니다.", - "u_tu": '<p class="warn">경고: 터보가 활성화되어 클라이언트가 불완전한 업로드를 감지하고 재개하지 못할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>', - "u_ts": '<p class="warn">경고: 터보가 활성화되어 검색 결과가 부정확할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>', + "u_tu": '<p class="warn">경고: 터보가 활성화되어 <span>클라이언트가 불완전한 업로드를 감지하고 재개하지 못할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>', + "u_ts": '<p class="warn">경고: 터보가 활성화되어 <span>검색 결과가 부정확할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>', "u_turbo_c": "터보가 서버 구성에서 비활성화되었습니다", "u_turbo_g": '이 볼륨 내에서 디렉터리 목록 권한이 없으므로\n터보를 비활성화합니다', "u_life_cfg": '자동 삭제 시간 <input id="lifem" p="60" /> 분 (또는 <input id="lifeh" p="3600" /> 시간)', From af8620da924f48513997381904323065bc6d4785 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 19:50:50 +0000 Subject: [PATCH 039/154] fix `--lang` helptext; closes #594 --- copyparty/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 38f786d8..b538b6a9 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1575,7 +1575,7 @@ def add_ui(ap, retry): ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)") 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("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m") + 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..7)") ap2.add_argument("--themes", metavar="NUM", type=int, default=10, help="number of themes installed") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") From d56230573dc63f9aa1ee5f3100112e3f00886894 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 20:02:32 +0000 Subject: [PATCH 040/154] separate audio-transcode timeout (#598) --- copyparty/__main__.py | 3 ++- copyparty/authsrv.py | 2 +- copyparty/cfg.py | 4 +++- copyparty/th_srv.py | 26 +++++++++++++------------- tests/util.py | 2 +- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index b538b6a9..4ae60e61 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1435,7 +1435,8 @@ def add_thumbnail(ap): ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") - ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)") + ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)") + ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 63613cc8..d65169ed 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2220,7 +2220,7 @@ class AuthSrv(object): if k in vol.flags: vol.flags[k] = int(vol.flags[k]) - zs = "convt tail_fd tail_rate tail_tmax" + zs = "aconvt convt tail_fd tail_rate tail_tmax" for k in zs.split(): if k in vol.flags: vol.flags[k] = float(vol.flags[k]) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 8a07f956..5b1651c4 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -68,6 +68,7 @@ def vf_bmap() -> dict[str, str]: def vf_vmap() -> dict[str, str]: """argv-to-volflag: simple values""" ret = { + "ac_convt": "aconvt", "no_hash": "nohash", "no_idx": "noidx", "re_maxage": "scan", @@ -260,7 +261,8 @@ flagcats = { "thsize": "thumbnail res; WxH", "crop": "center-cropping (y/n/fy/fn)", "th3x": "3x resolution (y/n/fy/fn)", - "convt": "conversion timeout in seconds", + "convt": "convert-to-image timeout in seconds", + "aconvt": "convert-to-audio timeout in seconds", "ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s", }, "handlers\n(better explained in --help-handlers)": { diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 537c748a..2522f3a6 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -662,11 +662,11 @@ class ThumbSrv(object): ] cmd += [fsenc(tpath)] - self._run_ff(cmd, vn) + self._run_ff(cmd, vn, "convt") - def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None: + def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None: # self.log((b" ".join(cmd)).decode("utf-8")) - ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom) + ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom) if not ret: return @@ -748,7 +748,7 @@ class ThumbSrv(object): # fmt: on cmd += [fsenc(tpath)] - self._run_ff(cmd, vn) + self._run_ff(cmd, vn, "convt") if "pngquant" in vn.flags: wtpath = tpath + ".png" @@ -809,7 +809,7 @@ class ThumbSrv(object): b"-y", fsenc(infile), ] # fmt: on - self._run_ff(cmd, vn) + self._run_ff(cmd, vn, "convt") fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" if "3" in fmt: @@ -851,7 +851,7 @@ class ThumbSrv(object): ] cmd += [fsenc(tpath)] - self._run_ff(cmd, vn) + self._run_ff(cmd, vn, "convt") def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: quality = self.args.q_mp3.lower() @@ -890,7 +890,7 @@ class ThumbSrv(object): fsenc(tpath) ] # fmt: on - self._run_ff(cmd, vn, oom=300) + self._run_ff(cmd, vn, "aconvt", oom=300) def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: if self.args.no_acode or not self.args.allow_flac: @@ -915,7 +915,7 @@ class ThumbSrv(object): fsenc(tpath) ] # fmt: on - self._run_ff(cmd, vn, oom=300) + self._run_ff(cmd, vn, "aconvt", oom=300) def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: if self.args.no_acode or not self.args.allow_wav: @@ -950,7 +950,7 @@ class ThumbSrv(object): fsenc(tpath) ] # fmt: on - self._run_ff(cmd, vn, oom=300) + self._run_ff(cmd, vn, "aconvt", oom=300) def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: if self.args.no_acode or not self.args.q_opus: @@ -1006,7 +1006,7 @@ class ThumbSrv(object): fsenc(tpath) ] # fmt: on - self._run_ff(cmd, vn, oom=300) + self._run_ff(cmd, vn, "aconvt", oom=300) def _conv_caf( self, @@ -1046,7 +1046,7 @@ class ThumbSrv(object): fsenc(tmp_opus) ] # fmt: on - self._run_ff(cmd, vn, oom=300) + self._run_ff(cmd, vn, "aconvt", oom=300) # iOS fails to play some "insufficiently complex" files # (average file shorter than 8 seconds), so of course we @@ -1073,7 +1073,7 @@ class ThumbSrv(object): fsenc(tpath) ] # fmt: on - self._run_ff(cmd, vn, oom=300) + self._run_ff(cmd, vn, "aconvt", oom=300) else: # simple remux should be safe @@ -1092,7 +1092,7 @@ class ThumbSrv(object): fsenc(tpath) ] # fmt: on - self._run_ff(cmd, vn, oom=300) + self._run_ff(cmd, vn, "aconvt", oom=300) try: wunlink(self.log, tmp_opus, vn.flags) diff --git a/tests/util.py b/tests/util.py index 16c75e92..80e3067e 100644 --- a/tests/util.py +++ b/tests/util.py @@ -158,7 +158,7 @@ class Cfg(Namespace): ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz" ka.update(**{k: 1 for k in ex.split()}) - ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" + ex = "ac_convt au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" ka.update(**{k: 9 for k in ex.split()}) ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" From 3aa8b7aa2d75091cc988e864fb89f344124ebbce Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 20:31:58 +0000 Subject: [PATCH 041/154] ftp: reject uploads nicely; closes #573 if a client tries to upload where it does not have write-access, rather than kicking the client with an exception, reply properly --- copyparty/ftpd.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 004fb492..99b37d26 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -285,9 +285,12 @@ class FtpFs(AbstractedFS): # returning 550 is library-default and suitable raise FSE("No such file or directory") - avfs = vfs.chk_ap(ap, st) - if not avfs: - raise FSE("Permission denied", 1) + if vfs.realpath: + avfs = vfs.chk_ap(ap, st) + if not avfs: + raise FSE("Permission denied", 1) + else: + avfs = vfs self.cwd = nwd ( @@ -492,7 +495,11 @@ class FtpHandler(FTPHandler): def ftp_STOR(self, file: str, mode: str = "w") -> Any: # Optional[str] vp = join(self.fs.cwd, file).lstrip("/") - ap, vfs, rem = self.fs.v2a(vp, w=True) + try: + ap, vfs, rem = self.fs.v2a(vp, w=True) + except Exception as ex: + self.respond("550 %s" % (ex,), logging.info) + return self.vfs_map[ap] = vp xbu = vfs.flags.get("xbu") if xbu and not runhook( From f4a3fba29c9e58ca76d92e8e0a70d3083406b048 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 14 Aug 2025 21:53:57 +0000 Subject: [PATCH 042/154] add global-logout button --- copyparty/web/splash.css | 4 ++ copyparty/web/splash.html | 9 +++- copyparty/web/splash.js | 88 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css index 5ccebdc7..3b4d0f68 100644 --- a/copyparty/web/splash.css +++ b/copyparty/web/splash.css @@ -24,6 +24,7 @@ h1 { li { margin: 1em 0; } +#lo, a { color: #047; background: #fff; @@ -47,6 +48,7 @@ td a { float: right; margin: -.2em 0 0 .8em; } +#lo, .logout, a.r { color: #c04; @@ -176,12 +178,14 @@ html.z { html.z h1 { border-color: #777; } +html.z #lo, html.z a { color: #fff; background: #057; border-color: #37a; } html.z .logout, +html.z #lo, html.z a.r { background: #804; border-color: #c28; diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 4363efd2..0a886ad4 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -22,7 +22,7 @@ <p id="b">howdy stranger   <small>(you're not logged in)</small></p> {%- else %} <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> - <p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p> + <p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p> {%- endif %} {%- endif %} @@ -168,6 +168,13 @@ <li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li> <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li> + + {%- if this.uname != '*' %} + <li><form method="post" enctype="multipart/form-data"> + <input type="hidden" name="act" value="logout" /> + <input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" /> + </form></li> + {% endif %} </ul> </div> diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index aad6ad2a..ad2d81db 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -17,6 +17,11 @@ var Ls = { "j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig", "k1": "nullstill innstillinger", "l1": "logg inn:", + "ls3": "logg inn", + "lu4": "brukernavn", + "lp4": "passord", + "lo3": "logg ut “{0}” overalt", + "lo2": "avslutter økten på alle nettlesere", "m1": "velkommen tilbake,", "n1": "404: filen finnes ikke  ┐( ´ -`)┌", "o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>', @@ -46,6 +51,7 @@ var Ls = { "eng": { "d2": "shows the state of all active threads", "e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect", + "lo2": "ends the session on all browsers", "u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds", "v2": "use this server as a local HDD", "ta1": "fill in your new password first", @@ -68,6 +74,11 @@ var Ls = { "j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。", "k1": "重置设置", "l1": "登录:", + "ls3": "登录", //m + "lu4": "用户名", //m + "lp4": "密码", //m + "lo3": "在所有地方注销 {0}", //m + "lo2": "这将结束在所有浏览器中的会话", //m "m1": "欢迎回来,", "n1": "404: 文件不存在  ┐( ´ -`)┌", "o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>', @@ -110,6 +121,11 @@ var Ls = { "j1": "povolení k304 odpojí vašeho klienta při každém HTTP 304, což může zabránit některým chybovým proxy serverům, aby se zasekly (náhle nenačítaly stránky), <em>ale</em> také to obecně zpomalí věci", "k1": "resetovat nastavení klienta", "l1": "přihlaste se pro více:", + "ls3": "přihlásit se", //m + "lu4": "uživatelské jméno", //m + "lp4": "heslo", //m + "lo3": "odhlásit “{0}” všude", //m + "lo2": "tímto ukončíte relaci ve všech prohlížečích", //m "m1": "vítej zpět,", "n1": "404 nenalezeno  ┐( ´ -`)┌", "o1": 'nebo možná nemáš přístup -- zkus heslo nebo <a href="' + SR + '/?h">jdi domů</a>', @@ -151,6 +167,11 @@ var Ls = { "j1": "k304 trennt die Clientverbindung bei jedem HTTP 304, was Bugs mit problematischen Proxies vorbeugen kann (z.B. nicht ladenden Seiten), macht Dinge aber generell langsamer", "k1": "Client-Einstellungen zurücksetzen", "l1": "Melde dich an für mehr:", + "ls3": "Anmelden", //m + "lu4": "Benutzername", //m + "lp4": "Passwort", //m + "lo3": "“{0}” überall abmelden", //m + "lo2": "Dies beendet die Sitzung in allen Browsern", //m "m1": "Willkommen zurück,", "n1": "404 Nicht gefunden  ┐( ´ -`)┌", "o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>', @@ -192,6 +213,11 @@ var Ls = { "j1": "k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, <em>mutta</em> se myös vähentää suorituskykyä", "k1": "nollaa asetukset", "l1": "kirjaudu sisään:", + "ls3": "kirjaudu sisään", //m + "lu4": "käyttäjätunnus", //m + "lp4": "salasana", //m + "lo3": "kirjaa “{0}” ulos kaikkialta", //m + "lo2": "tämä lopettaa istunnon kaikissa selaimissa", //m "m1": "tervetuloa takaisin,", "n1": "404: ei löytynyt mitään  ┐( ´ -`)┌", "o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>', @@ -234,6 +260,11 @@ var Ls = { "j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), <em>mais</em> cela ralentira également les choses en général", "k1": "réinitialiser les paramètres du client", "l1": "connectez-vous pour en savoir plus :", + "ls3": "se connecter", //m + "lu4": "nom d'utilisateur", //m + "lp4": "mot de passe", //m + "lo3": "déconnecter “{0}” partout", //m + "lo2": "cela mettra fin à la session sur tous les navigateurs", //m "m1": "heureux de vous revoir,", "n1": "404 introuvable  ┐( ´ -`)┌", "o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>', @@ -275,6 +306,11 @@ var Ls = { "j1": "η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), <em>αλλά</em> θα κάνει τα πράγματα, γενικά πιο αργά", "k1": "επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη", "l1": "συνδέσου για περισσότερα:", + "ls3": "σύνδεση", //m + "lu4": "όνομα χρήστη", //m + "lp4": "κωδικός πρόσβασης", //m + "lo3": "αποσύνδεση του “{0}” από παντού", //m + "lo2": "αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές", //m "m1": "καλώς ήρθες,", "n1": "404 δεν βρέθηκε  ┐( ´ -`)┌", "o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>', @@ -316,6 +352,11 @@ var Ls = { "j1": "k304 interrompe la connessione per ogni HTTP 304. Questo aiuta contro alcuni proxy difettosi che possono bloccarsi o smettere improvvisamente di caricare pagine, ma riduce notevolmente le prestazioni", "k1": "resetta impostazioni", "l1": "accedi:", + "ls3": "accedi", //m + "lu4": "nome utente", //m + "lp4": "password", //m + "lo3": "disconnetti “{0}” ovunque", //m + "lo2": "questo terminerà la sessione su tutti i browser", //m "m1": "bentornato,", "n1": "404: file non trovato  ┐( ´ -`)┌", "o1": "oppure forse non hai accesso? prova una password o <a href=\"SR/?h\">torna alla home</a>", @@ -358,6 +399,11 @@ var Ls = { "j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>", "k1": "클라이언트 설정 초기화", "l1": "로그인하기:", + "ls3": "로그인", //m + "lu4": "사용자 이름", //m + "lp4": "비밀번호", //m + "lo3": "{0}을(를) 모든 곳에서 로그아웃", //m + "lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m "m1": "또 오셨네요,", "n1": "404 찾을 수 없음  ┐( ´ -`)┌", "o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요", @@ -400,6 +446,11 @@ var Ls = { "j1": "k304 verbreekt de verbinding voor elke HTTP 304. Dit helpt tegen bepaalde proxy servers die kunnen vastlopen/plotseling stoppen met het laden van pagina's, maar het vermindert ook de prestaties aanzienlijk", "k1": "Instellingen resetten", "l1": "Inloggen:", + "ls3": "inloggen", //m + "lu4": "gebruikersnaam", //m + "lp4": "wachtwoord", //m + "lo3": "“{0}” overal afmelden", //m + "lo2": "dit zal de sessie in alle browsers beëindigen", //m "m1": "Welkom terug,", "n1": "404: bestand bestaat niet  ┐( ´ -`)┌", "o1": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>', @@ -442,6 +493,11 @@ var Ls = { "j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig", "k1": "nullstill innstillinger", "l1": "logg inn:", + "ls3": "logg inn", + "lu4": "brukarnamn", + "lp4": "passord", + "lo3": "logg ut “{0}” overalt", + "lo2": "avslutt økta på alle nettlesarar", "m1": "velkomen attende,", "n1": "404: filen finnast ikkje  ┐( ´ -`)┌", "o1": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>', @@ -484,6 +540,11 @@ var Ls = { "j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, <em>ale</em> spowolni ogólne działanie", "k1": "zresetuj ustawienia klienta", "l1": "zaloguj się po więcej:", + "ls3": "zaloguj się", //m + "lu4": "nazwa użytkownika", //m + "lp4": "hasło", //m + "lo3": "wyloguj “{0}” wszędzie", //m + "lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m "m1": "Witaj,", "n1": "404 nie znaleziono  ┐( ´ -`)┌", "o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href="' + SR + '/?h">przejdź do strony głównej</a>', @@ -526,6 +587,11 @@ var Ls = { "j1": "activar k304 desconectará tu cliente en cada HTTP 304, lo que puede evitar que algunos proxies con errores se atasquen (dejando de cargar páginas de repente), <em>pero</em> también ralentizará las cosas en general", "k1": "restablecer config. de cliente", "l1": "inicia sesión para más:", + "ls3": "iniciar sesión", //m + "lu4": "nombre de usuario", //m + "lp4": "contraseña", //m + "lo3": "cerrar sesión de “{0}” en todas partes", //m + "lo2": "esto finalizará la sesión en todos los navegadores", //m "m1": "bienvenido de nuevo,", "n1": "404 no encontrado  ┐( ´ -`)┌", "o1": '¿o quizás no tienes acceso? -- prueba con una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>', @@ -568,6 +634,11 @@ var Ls = { "j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet", "k1": "återställ klientinställningar", "l1": "logga in för att se mer:", + "ls3": "logga in", //m + "lu4": "användarnamn", //m + "lp4": "lösenord", //m + "lo3": "logga ut “{0}” överallt", //m + "lo2": "avsluta sessionen i alla webbläsare", //m "m1": "välkommen tillbaka,", "n1": "404 hittades inte  ┐( ´ -`)┌", "o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>', @@ -610,6 +681,11 @@ var Ls = { "j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом", "k1": "скинути налаштування клієнта", "l1": "авторизуйтесь для інших опцій:", + "ls3": "увійти", //m + "lu4": "ім'я користувача", //m + "lp4": "пароль", //m + "lo3": "вийти з облікового запису “{0}” всюди", //m + "lo2": "це завершить сеанс у всіх браузерах", //m "m1": "з поверненням,", "n1": "404 не знайдено  ┐( ´ -`)┌", "o1": 'або у вас немає доступу -- спробуйте авторизуватися або <a href="' + SR + '/?h">повернутися на головну</a>', @@ -652,6 +728,11 @@ var Ls = { "j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее", "k1": "сбросить локальные настройки", "l1": "авторизуйтесь для других опций:", + "ls3": "войти", //m + "lu4": "имя пользователя", //m + "lp4": "пароль", //m + "lo3": "выйти из “{0}” везде", //m + "lo2": "это завершит сеанс во всех браузерах", //m "m1": "с возвращением,", "n1": "404 не найдено  ┐( ´ -`)┌", "o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>', @@ -696,7 +777,14 @@ for (var k in (d || {})) { o[a].innerHTML = d[k]; else if (f == 2) o[a].setAttribute("tt", d[k]); + else if (f == 3) + o[a].setAttribute("value", d[k]); + else if (f == 4) + o[a].setAttribute("placeholder", " " + d[k]); } +var o1 = ebi('lo'), o2 = ebi('un'); +if (o1 && o2 && d.lo3) + o1.setAttribute("value", d.lo3.format(o2.textContent)); try { if (is_idp) { From a4649d1e71308f3b663e95f1a26762f63d9d5a81 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 15 Aug 2025 19:19:21 +0000 Subject: [PATCH 043/154] generic header auth (closes #504); extends idp-auth to also accept a collection of headers (and expected values of those headers) and map those to certain users useful for Tailscale-User-Login and similar --- README.md | 15 ++++++++++ copyparty/__main__.py | 64 ++++++++++++++++++++++++++++++++++++++++- copyparty/authsrv.py | 12 ++++---- copyparty/httpcli.py | 18 ++++++++++-- copyparty/svchub.py | 19 ++++++++++-- copyparty/up2k.py | 2 +- copyparty/web/svcs.html | 2 +- tests/test_idp.py | 2 +- tests/util.py | 4 +-- 9 files changed, 122 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 16bfb15f..f6ed75c6 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ made in Norway 🇳🇴 * [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/)) * [ip auth](#ip-auth) - autologin based on IP range (CIDR) * [identity providers](#identity-providers) - replace copyparty passwords with oauth and such + * [generic header auth](#generic-header-auth) - other ways to auth by header * [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords * [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar * [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed @@ -1915,6 +1916,20 @@ a more complete example of the copyparty configuration options [look like this]( but if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead +### generic header auth + +other ways to auth by header + +if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file: + +```yaml +[global] + idp-hm-usr: ^Tailscale-User-Login^alice.m@forest.net^alice +``` + +repeat the whole `idp-hm-usr` option to add more mappings + + ## user-changeable passwords if permitted, users can change their own passwords in the control-panel diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 4ae60e61..3c89af99 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -614,6 +614,36 @@ def get_sects(): consider the config file for more flexible account/volume management, including dynamic reload at runtime (and being more readable w) + + see \033[32m--help-auth\033[0m for ways to provide the password in requests; + see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares + """ + ), + ], + [ + "auth", + "how to login from a client", + dedent( + """ + different ways to provide the password so you become authenticated: + + login with the ui: + go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there + + send the password in the '\033[36mPW\033[0m' http-header: + \033[36mPW: \033[35mhunter2\033[0m + or if you have \033[33m--accounts\033[0m enabled, + \033[36mPW: \033[35med:hunter2\033[0m + + send the password in the URL itself: + \033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m + or if you have \033[33m--accounts\033[0m enabled, + \033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m + + use basic-authentication: + \033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m + which should be the same as this header: + \033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m """ ), ], @@ -765,6 +795,36 @@ def get_sects(): the upload speed can easily drop to 10% for small files)""" ), ], + [ + "idp", + "replacing the login system with fancy middleware", + dedent( + """ + if you already have a centralized service which handles + user-authentication for other services already, you can + integrate copyparty with that for automatic login + + if the middleware is providing the username in an http-header + named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m + + if the middleware is providing a list of groups in the header + named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m + + if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m + + if the middleware is providing a header named '\033[35mAccount\033[0m' + and the value is '\033[35malice@forest.net\033[0m' but the username is + actually '\033[35mmarisa\033[0m' then do this for each user: + \033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m + (the separator '\033[35m^\033[0m' can be any character) + + make ABSOLUTELY SURE that the header can only be set by your + middleware and not by clients! and, as an extra precaution, + send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword) + and then \033[36m--idp-h-key finalmasterspark\033[0m to require that + """ + ), + ], [ "urlform", "how to handle url-form POSTs", @@ -1153,7 +1213,8 @@ def add_auth(ap): idp_db = os.path.join(E.cfg, "idp.db") ses_db = os.path.join(E.cfg, "sessions.db") ap2 = ap.add_argument_group("IdP / identity provider / user authentication options") - ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy") + ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy") + ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp") ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control") ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") @@ -1168,6 +1229,7 @@ def add_auth(ap): ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies") ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known") ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]") + ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS) def add_chpw(ap): diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index d65169ed..09c845f8 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1689,6 +1689,8 @@ class AuthSrv(object): self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns))) raise + self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr) + self.setup_pwhash(acct) defpw = acct.copy() self.setup_chpw(acct) @@ -1701,7 +1703,7 @@ class AuthSrv(object): mount = cased - if not mount and not self.args.idp_h_usr: + if not mount and not self.args.have_idp_hdrs: # -h says our defaults are CWD at root and read/write for everyone axs = AXS(["*"], ["*"], None, None) ehint = "" @@ -1874,7 +1876,7 @@ class AuthSrv(object): if missing_users: zs = ", ".join(k for k in sorted(missing_users)) - if self.args.idp_h_usr: + if self.args.have_idp_hdrs: t = "the following users are unknown, and assumed to come from IdP: " self.log(t + zs, c=6) else: @@ -2551,7 +2553,7 @@ class AuthSrv(object): if not self.args.no_voldump: self.log(t) - if have_e2d or self.args.idp_h_usr: + if have_e2d or self.args.have_idp_hdrs: t = self.chk_sqlite_threadsafe() if t: self.log("\n\033[{}\033[0m\n".format(t)) @@ -2841,7 +2843,7 @@ class AuthSrv(object): def load_idp_db(self, quiet=False) -> None: # mutex me level = self.args.idp_store - if level < 2 or not self.args.idp_h_usr: + if level < 2 or not self.args.have_idp_hdrs: return assert sqlite3 # type: ignore # !rm @@ -2898,7 +2900,7 @@ class AuthSrv(object): n = [] q = "insert into us values (?,?,?)" accs = list(self.acct) - if self.args.idp_h_usr and self.args.idp_cookie: + if self.args.have_idp_hdrs and self.args.idp_cookie: accs.extend(self.idp_accs.keys()) for uname in accs: if uname not in ases: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index c262dc2e..451f6afe 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -624,8 +624,22 @@ class HttpCli(object): or "*" ) - if self.args.idp_h_usr: - idp_usr = self.headers.get(self.args.idp_h_usr) or "" + if self.args.have_idp_hdrs: + idp_usr = "" + if self.args.idp_hm_usr: + for hn, hmv in self.args.idp_hm_usr_p.items(): + zs = self.headers.get(hn) + if zs: + for zs1, zs2 in hmv.items(): + if zs == zs1: + idp_usr = zs2 + break + if idp_usr: + break + for hn in self.args.idp_h_usr: + if idp_usr: + break + idp_usr = self.headers.get(hn) if idp_usr: idp_grp = ( self.headers.get(self.args.idp_h_grp) or "" diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 950122d0..f2737b06 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -243,7 +243,7 @@ class SvcHub(object): t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s" self.log("root", t % (args.th_ram_max, zs), 3) - if args.chpw and args.idp_h_usr: + if args.chpw and args.have_idp_hdrs: t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr" self.log("root", t, 1) raise Exception(t) @@ -268,7 +268,7 @@ class SvcHub(object): args.no_ses = True args.shr = "" - if args.idp_store and args.idp_h_usr: + if args.idp_store and args.have_idp_hdrs: self.setup_db("idp") if not self.args.no_ses: @@ -1011,10 +1011,23 @@ class SvcHub(object): al.sus_urls = None al.xff_hdr = al.xff_hdr.lower() - al.idp_h_usr = al.idp_h_usr.lower() + al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []] al.idp_h_grp = al.idp_h_grp.lower() al.idp_h_key = al.idp_h_key.lower() + al.idp_hm_usr_p = {} + for zs0 in al.idp_hm_usr or []: + try: + sep = zs0[:1] + hn, zs1, zs2 = zs0[1:].split(sep) + hn = hn.lower() + if hn in al.idp_hm_usr_p: + al.idp_hm_usr_p[hn][zs1] = zs2 + else: + al.idp_hm_usr_p[hn] = {zs1: zs2} + except: + raise Exception("invalid --idp-hm-usr [%s]" % (zs0,)) + al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True) al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 993d5b82..f4d6d66f 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -903,7 +903,7 @@ class Up2k(object): self.iacct = self.asrv.iacct self.grps = self.asrv.grps - have_e2d = self.args.idp_h_usr or self.args.chpw or self.args.shr + have_e2d = self.args.have_idp_hdrs or self.args.chpw or self.args.shr vols = list(all_vols.values()) t0 = time.time() diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index 26fd5267..9d139a7b 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -42,7 +42,7 @@ - {% if args.idp_h_usr %} + {% if args.have_idp_hdrs %} <p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p> {% endif %} diff --git a/tests/test_idp.py b/tests/test_idp.py index bd81378f..21d46758 100644 --- a/tests/test_idp.py +++ b/tests/test_idp.py @@ -63,7 +63,7 @@ class TestVFS(unittest.TestCase): cfgdir = os.path.join(here, "res", "idp") # globals are applied by main so need to cheat a little - xcfg = {"idp_h_usr": "x-idp-user", "idp_h_grp": "x-idp-group"} + xcfg = {"idp_h_usr": ["x-idp-user"], "idp_h_grp": "x-idp-group"} return here, cfgdir, xcfg diff --git a/tests/util.py b/tests/util.py index 80e3067e..8f4cd0e9 100644 --- a/tests/util.py +++ b/tests/util.py @@ -164,13 +164,13 @@ class Cfg(Namespace): ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin 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 chmod_f chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" + ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner" ka.update(**{k: "no" for k in ex.split()}) - ex = "ext_th grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" + ex = "ext_th grp idp_h_usr idp_hm_usr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" ka.update(**{k: [] for k in ex.split()}) ex = "exp_lg exp_md" From 62e072a2ed14a96386509581616fde1545454480 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 15 Aug 2025 20:12:17 +0000 Subject: [PATCH 044/154] restrict account to ip/subnet; closes #397 --- README.md | 15 +++++++++++++++ copyparty/__main__.py | 2 ++ copyparty/authsrv.py | 1 + copyparty/ftpd.py | 4 ++++ copyparty/httpcli.py | 10 ++++++++-- copyparty/httpsrv.py | 6 ++++++ copyparty/svchub.py | 8 ++++++++ copyparty/util.py | 21 +++++++++++++++++++++ tests/util.py | 2 +- 9 files changed, 66 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6ed75c6..1d143b6d 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ made in Norway 🇳🇴 * [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/)) * [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/)) * [ip auth](#ip-auth) - autologin based on IP range (CIDR) + * [restrict to ip](#restrict-to-ip) - limit a user to certain IP ranges (CIDR) * [identity providers](#identity-providers) - replace copyparty passwords with oauth and such * [generic header auth](#generic-header-auth) - other ways to auth by header * [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords @@ -1897,6 +1898,20 @@ repeat the option to map additional subnets **be careful with this one!** if you have a reverseproxy, then you definitely want to make sure you have [real-ip](#real-ip) configured correctly, and it's probably a good idea to nullmap the reverseproxy's IP just in case; so if your reverseproxy is sending requests from `172.24.27.9` then that would be `--ipu=172.24.27.9/32=` +### restrict to ip + +limit a user to certain IP ranges (CIDR) , using the global-option `--ipr` + +for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file: + +```yaml +[global] + ipr: 192.168.123.0/24,172.16.0.0/16=spartacus +``` + +repeat the option to map additional users + + ## identity providers replace copyparty passwords with oauth and such diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3c89af99..e732369b 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1229,7 +1229,9 @@ def add_auth(ap): ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies") ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known") ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]") + ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]") ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS) def add_chpw(ap): diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 09c845f8..5329ede1 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1690,6 +1690,7 @@ class AuthSrv(object): raise self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr) + self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr) self.setup_pwhash(acct) defpw = acct.copy() diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 99b37d26..14f8d5b6 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -96,6 +96,10 @@ class FtpAuth(DummyAuthorizer): if args.ipu and uname == "*": uname = args.ipu_iu[args.ipu_nm.map(ip)] + if args.ipr and uname in args.ipr_u: + if not args.ipr_u[uname].map(ip): + logging.warning("username [%s] rejected by --ipr", uname) + uname = "*" if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): g = self.hub.gpwd diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 451f6afe..4710feaf 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -695,8 +695,14 @@ class HttpCli(object): else: self.log("unknown username: %r" % (idp_usr,), 1) - if self.args.ipu and self.uname == "*": - self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)] + if self.args.have_ipu_or_ipr: + if self.args.ipu and self.uname == "*": + self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)] + ipr = self.conn.hsrv.ipr + if ipr and self.uname in ipr: + if not ipr[self.uname].map(self.ip): + self.log("username [%s] rejected by --ipr" % (self.uname,), 3) + self.uname = "*" self.rvol = self.asrv.vfs.aread[self.uname] self.wvol = self.asrv.vfs.awrite[self.uname] diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index ddf5e7bc..77492d56 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -70,6 +70,7 @@ from .util import ( build_netmap, has_resource, ipnorm, + load_ipr, load_ipu, load_resource, min_ex, @@ -193,6 +194,11 @@ class HttpSrv(object): else: self.ipu_iu = self.ipu_nm = None + if self.args.ipr: + self.ipr = load_ipr(self.log, self.args.ipr) + else: + self.ipr = None + self.ipa_nm = build_netmap(self.args.ipa) self.xff_nm = build_netmap(self.args.xff_src) self.xff_lan = build_netmap("lan") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index f2737b06..7b06d96c 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -66,6 +66,7 @@ from .util import ( build_netmap, expat_ver, gzip, + load_ipr, load_ipu, lock_file, min_ex, @@ -259,6 +260,10 @@ class SvcHub(object): setattr(args, "ipu_iu", iu) setattr(args, "ipu_nm", nm) + if args.ipr: + ipr = load_ipr(self.log, args.ipr, True) + setattr(args, "ipr_u", ipr) + for zs in "ah_salt fk_salt dk_salt".split(): if getattr(args, "show_%s" % (zs,)): self.log("root", "effective %s is %s" % (zs, getattr(args, zs))) @@ -432,6 +437,9 @@ class SvcHub(object): getattr(args, zs).mutex = threading.Lock() except: pass + if args.ipr: + for nm in args.ipr_u.values(): + nm.mutex = threading.Lock() def _db_onfail_ses(self) -> None: self.args.no_ses = True diff --git a/copyparty/util.py b/copyparty/util.py index 1578d291..03517785 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -2954,6 +2954,27 @@ def load_ipu( return ip_u, nm +def load_ipr( + log: "RootLogger", iprs: list[str], defer_mutex: bool = False +) -> dict[str, NetMap]: + ret = {} + for ipr in iprs: + try: + zs, uname = ipr.split("=") + cidrs = zs.split(",") + except: + t = "\n invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)" + raise Exception(t % (ipr,)) + try: + nm = NetMap(["::"], cidrs, True, True, defer_mutex) + except Exception as ex: + t = "failed to translate --ipr into netmap, probably due to invalid config: %r" + log("root", t % (ex,), 1) + raise + ret[uname] = nm + return ret + + def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]: readsz = min(bufsz, 128 * 1024) with open(fsenc(fn), "rb", bufsz) as f: diff --git a/tests/util.py b/tests/util.py index 8f4cd0e9..16a0132a 100644 --- a/tests/util.py +++ b/tests/util.py @@ -170,7 +170,7 @@ class Cfg(Namespace): ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner" ka.update(**{k: "no" for k in ex.split()}) - ex = "ext_th grp idp_h_usr idp_hm_usr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" + ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" ka.update(**{k: [] for k in ex.split()}) ex = "exp_lg exp_md" From 1228b5510b518bacf10baf07ed15d33ce5817f2e Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 15 Aug 2025 20:14:35 +0000 Subject: [PATCH 045/154] option -ss requires webdav login; closes #613 --- copyparty/__main__.py | 2 +- copyparty/svchub.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index e732369b..dcd78d8c 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1409,7 +1409,7 @@ def add_optouts(ap): def add_safety(ap): ap2 = ap.add_argument_group("safety options") ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js") - ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih") + ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih") ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r") 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)") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 7b06d96c..8661bc29 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -156,6 +156,7 @@ class SvcHub(object): args.no_del = True args.no_mv = True args.hardlink = True + args.dav_auth = True args.vague_403 = True args.nih = True From 4df033ecc3888de1fe17cba39e28b6dee98f2242 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 15 Aug 2025 21:33:13 +0000 Subject: [PATCH 046/154] [DB-V6]: store usernames; closes #530 --- copyparty/__main__.py | 1 + copyparty/cfg.py | 2 + copyparty/httpcli.py | 39 +++++++++++++--- copyparty/u2idx.py | 2 +- copyparty/up2k.py | 105 +++++++++++++++++++++++++++--------------- copyparty/util.py | 2 +- copyparty/web/rups.js | 3 +- 7 files changed, 108 insertions(+), 46 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index dcd78d8c..01216722 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1120,6 +1120,7 @@ def add_upload(ap): 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)") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h") + ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)") ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)") ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600") diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 5b1651c4..3a7a205b 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -118,6 +118,7 @@ def vf_vmap() -> dict[str, str]: "u2ts", "uid", "gid", + "unp_who", "ups_who", "zip_who", "zipmaxn", @@ -343,6 +344,7 @@ flagcats = { "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', "rss": "allow '?rss' URL suffix (experimental)", "rmagic": "expensive analysis for mimetype accuracy", + "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name", "ups_who=2": "restrict viewing the list of recent uploads", "zip_who=2": "restrict access to download-as-zip/tar", "zipmaxn=9k": "reject download-as-zip if more than 9000 files", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 4710feaf..de3e6ede 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5501,6 +5501,10 @@ class HttpCli(object): and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv) ] + q = "" + qp = (0,) + q_c = -1 + for vol in allvols: cur = idx.get_cur(vol) if not cur: @@ -5508,9 +5512,23 @@ class HttpCli(object): nfk, fk_alg = fk_vols.get(vol) or (0, 0) + zi = vol.flags["unp_who"] + if q_c != zi: + q_c = zi + q = "select sz, rd, fn, at from up where " + if zi == 1: + q += "ip=? and un=?" + qp = (self.ip, self.uname, lim) + elif zi == 2: + q += "ip=?" + qp = (self.ip, lim) + if zi == 3: + q += "un=?" + qp = (self.uname, lim) + q += " and at>? order by at desc" + n = 2000 - q = "select sz, rd, fn, at from up where ip=? and at>? order by at desc" - for sz, rd, fn, at in cur.execute(q, (self.ip, lim)): + for sz, rd, fn, at in cur.execute(q, qp): vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) if nfi == 0 or (nfi == 1 and vfi in vp.lower()): pass @@ -5635,8 +5653,8 @@ class HttpCli(object): continue n = 1000 - q = "select sz, rd, fn, ip, at from up where at>0 order by at desc" - for sz, rd, fn, ip, at in cur.execute(q): + q = "select sz, rd, fn, ip, at, un from up where at>0 order by at desc" + for sz, rd, fn, ip, at, un in cur.execute(q): vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) if nfi == 0 or (nfi == 1 and vfi in vp.lower()): pass @@ -5657,6 +5675,7 @@ class HttpCli(object): "sz": sz, "ip": ip, "at": at, + "un": un, "nfk": nfk, "adm": adm, } @@ -5701,12 +5720,16 @@ class HttpCli(object): adm = rv.pop("adm") if not adm: rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)" + if rv["un"] not in ("*", self.uname): + rv["un"] = "(?)" else: for rv in ret: adm = rv.pop("adm") if not adm: rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)" rv["at"] = 0 + if rv["un"] not in ("*", self.uname): + rv["un"] = "(?)" if self.is_vproxied: for v in ret: @@ -6628,13 +6651,15 @@ class HttpCli(object): tags = {k: v for k, v in r} if is_admin: - q = "select ip, at from up where rd=? and fn=?" + q = "select ip, at, un from up where rd=? and fn=?" try: - zs1, zs2 = icur.execute(q, erd_efn).fetchone() + zs1, zs2, zs3 = icur.execute(q, erd_efn).fetchone() if zs1: tags["up_ip"] = zs1 if zs2: tags[".up_at"] = zs2 + if zs3: + tags["up_by"] = zs3 except: pass elif add_up_at: @@ -6655,7 +6680,7 @@ class HttpCli(object): lmte = list(mte) if self.can_admin: - lmte.extend(("up_ip", ".up_at")) + lmte.extend(("up_by", "up_ip", ".up_at")) if "nodirsz" not in vf: tagset.add(".files") diff --git a/copyparty/u2idx.py b/copyparty/u2idx.py index ff0e329f..45b479c5 100644 --- a/copyparty/u2idx.py +++ b/copyparty/u2idx.py @@ -391,7 +391,7 @@ class U2idx(object): fk_alg = 2 if "fka" in flags else 1 c = cur.execute(uq, tuple(vuv)) for hit in c: - w, ts, sz, rd, fn, ip, at = hit[:7] + w, ts, sz, rd, fn = hit[:5] if rd.startswith("//") or fn.startswith("//"): rd, fn = s3dec(rd, fn) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index f4d6d66f..7ae6441b 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -77,7 +77,7 @@ except: if HAVE_SQLITE3: import sqlite3 -DB_VER = 5 +DB_VER = 6 if True: # pylint: disable=using-constant-test from typing import Any, Optional, Pattern, Union @@ -1655,7 +1655,7 @@ class Up2k(object): abspath = cdirs + fn nohash = reh.search(abspath) if reh else False - sql = "select w, mt, sz, ip, at from up where rd = ? and fn = ?" + sql = "select w, mt, sz, ip, at, un from up where rd = ? and fn = ?" try: c = db.c.execute(sql, (rd, fn)) except: @@ -1664,7 +1664,7 @@ class Up2k(object): in_db = list(c.fetchall()) if in_db: self.pp.n -= 1 - dw, dts, dsz, ip, at = in_db[0] + dw, dts, dsz, ip, at, un = in_db[0] if len(in_db) > 1: t = "WARN: multiple entries: %r => %r |%d|\n%r" rep_db = "\n".join([repr(x) for x in in_db]) @@ -1677,6 +1677,9 @@ class Up2k(object): if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz): continue + if un is None: + un = "" + t = "reindex %r => %r mtime(%s/%s) size(%s/%s)" self.log(t % (top, rp, dts, lmod, dsz, sz)) self.db_rm(db.c, rd, fn, 0) @@ -1687,6 +1690,7 @@ class Up2k(object): dw = "" ip = "" at = 0 + un = "" self.pp.msg = "a%d %s" % (self.pp.n, abspath) @@ -1712,9 +1716,10 @@ class Up2k(object): if dw and dw != wark: ip = "" at = 0 + un = "" # skip upload hooks by not providing vflags - self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", "", ip, at) + self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at) db.n += 1 db.nf += 1 tfa += 1 @@ -2151,8 +2156,8 @@ class Up2k(object): with self.mutex: try: - q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?" - rd, fn, ip, at = cur.execute(q, (w16, w)).fetchone() + q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?" + rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone() except: # file modified/deleted since spooling continue @@ -2171,12 +2176,15 @@ 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) + n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un) else: + oth_tags = {} if ip: - oth_tags = {"up_ip": ip, "up_at": at} - else: - oth_tags = {} + oth_tags["up_ip"] = ip + if at: + oth_tags["up_at"] = at + if un: + oth_tags["up_by"] = un mpool.put(Mpqe({}, entags, w, abspath, oth_tags)) with self.mutex: @@ -2332,8 +2340,8 @@ class Up2k(object): if w in in_progress: continue - q = "select rd, fn, ip, at from up where substr(w,1,16)=? limit 1" - rd, fn, ip, at = cur.execute(q, (w,)).fetchone() + q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1" + rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone() rd, fn = s3dec(rd, fn) abspath = djoin(ptop, rd, fn) @@ -2357,7 +2365,10 @@ class Up2k(object): if ip: oth_tags["up_ip"] = ip + if at: oth_tags["up_at"] = at + if un: + oth_tags["up_by"] = un jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags)) in_progress[w] = True @@ -2546,6 +2557,7 @@ class Up2k(object): abspath: str, ip: str, at: float, + un: Optional[str], ) -> int: """will mutex(main)""" assert self.mtag # !rm @@ -2566,7 +2578,10 @@ class Up2k(object): if ip: tags["up_ip"] = ip + if at: tags["up_at"] = at + if un: + tags["up_by"] = un with self.mutex: return self._tag_file(write_cur, entags, wark, abspath, tags) @@ -2670,16 +2685,19 @@ class Up2k(object): if not existed and ver is None: return self._try_create_db(db_path, cur) - if ver == 4: + for upver in (4, 5): + if ver != upver: + continue try: t = "creating backup before upgrade: " cur = self._backup_db(db_path, cur, ver, t) - self._upgrade_v4(cur) - ver = 5 + getattr(self, "_upgrade_v%d" % (upver,))(cur) + ver += 1 # type: ignore except: - self.log("WARN: failed to upgrade from v4", 3) + self.log("WARN: failed to upgrade from v%d" % (ver,), 3) if ver == DB_VER: + # these no longer serve their intended purpose but they're great as additional sanchks self._add_dhash_tab(cur) self._add_xiu_tab(cur) self._add_cv_tab(cur) @@ -2781,7 +2799,7 @@ class Up2k(object): idx = r"create index up_w on up(w)" for cmd in [ - r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int)", + r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)", r"create index up_vp on up(rd, fn)", r"create index up_fn on up(fn)", r"create index up_ip on up(ip)", @@ -2814,6 +2832,15 @@ class Up2k(object): cur.connection.commit() + def _upgrade_v5(self, cur: "sqlite3.Cursor") -> None: + for cmd in [ + r"alter table up add column un text", + r"update kv set v=6 where k='sver'", + ]: + cur.execute(cmd) + + cur.connection.commit() + def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None: # v5 -> v5a try: @@ -3011,7 +3038,7 @@ class Up2k(object): argv = [dwark[:16], dwark] c2 = cur.execute(q, tuple(argv)) - for _, dtime, dsize, dp_dir, dp_fn, ip, at in c2: + for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2: if dp_dir.startswith("//") or dp_fn.startswith("//"): dp_dir, dp_fn = s3dec(dp_dir, dp_fn) @@ -3433,7 +3460,7 @@ class Up2k(object): try: vrel = vjoin(job["prel"], fname) xlink = bool(vf.get("xlink")) - cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel) + cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel) self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink) except Exception as ex: self.log("skipping replace-relink: %r" % (ex,)) @@ -3890,14 +3917,14 @@ class Up2k(object): # plugins may expect this to look like an actual IP db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip - sql = "insert into up values (?,?,?,?,?,?,?)" - v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0)) + sql = "insert into up values (?,?,?,?,?,?,?,?)" + v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) try: db.execute(sql, v) except: assert self.mem_cur # !rm rd, fn = s3enc(self.mem_cur, rd, fn) - v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0)) + v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) db.execute(sql, v) self.volsize[db] += sz @@ -4038,7 +4065,7 @@ class Up2k(object): vn, rem = vn0.get_dbv(rem0) ptop = vn.realpath with self.mutex, self.reg_mutex: - abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1) + abrt_cfg = vn.flags.get("u2abort", 1) addr = (ip or "\n") if abrt_cfg in (1, 2) else "" user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None reg = self.registry.get(ptop, {}) if abrt_cfg else {} @@ -4059,17 +4086,22 @@ class Up2k(object): if partial: dip = ip dat = time.time() + dun = uname + un_cfg = 1 else: - if not self.args.unpost: + un_cfg = vn.flags["unp_who"] + if not self.args.unpost or not un_cfg: t = "the unpost feature is disabled in server config" raise Pebkac(400, t) - _, _, _, _, dip, dat = self._find_from_vpath(ptop, rem) + _, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem) t = "you cannot delete this: " if not dip: t += "file not found" - elif dip != ip: + elif dip != ip and un_cfg in (1, 2): + t += "not uploaded by (You)" + elif dun != uname and un_cfg in (1, 3): t += "not uploaded by (You)" elif dat < time.time() - self.args.unpost: t += "uploaded too long ago" @@ -4158,7 +4190,7 @@ class Up2k(object): try: ptop = dbv.realpath xlink = bool(dbv.flags.get("xlink")) - cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath) + cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath) self._forget_file( ptop, volpath, cur, wark, True, st.st_size, xlink ) @@ -4319,7 +4351,7 @@ class Up2k(object): bos.makedirs(os.path.dirname(dabs), vf=dvn.flags) - c1, w, ftime_, fsize_, ip, at = self._find_from_vpath( + c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath( svn_dbv.realpath, srem_dbv ) c2 = self.cur.get(dvn.realpath) @@ -4344,7 +4376,7 @@ class Up2k(object): w, w, "", - "", + un or "", ip or "", at or 0, ) @@ -4605,7 +4637,7 @@ class Up2k(object): return "k" - c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem) + c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem) c2 = self.cur.get(dvn.realpath) has_dupes = False @@ -4639,7 +4671,7 @@ class Up2k(object): w, w, "", - "", + un or "", ip or "", at or 0, ) @@ -4739,13 +4771,14 @@ class Up2k(object): Optional[int], str, Optional[int], + str, ]: cur = self.cur.get(ptop) if not cur: - return None, None, None, None, "", None + return None, None, None, None, "", None, "" rd, fn = vsplit(vrem) - q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1" + q = "select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1" try: c = cur.execute(q, (rd, fn)) except: @@ -4754,9 +4787,9 @@ class Up2k(object): hit = c.fetchone() if hit: - wark, ftime, fsize, ip, at = hit - return cur, wark, ftime, fsize, ip, at - return cur, None, None, None, "", None + wark, ftime, fsize, ip, at, un = hit + return cur, wark, ftime, fsize, ip, at, un + return cur, None, None, None, "", None, "" def _forget_file( self, diff --git a/copyparty/util.py b/copyparty/util.py index 03517785..7e9d4bb1 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -3602,7 +3602,7 @@ def runihook( verbose: bool, cmd: str, vol: "VFS", - ups: list[tuple[str, int, int, str, str, str, int]], + ups: list[tuple[str, int, int, str, str, str, int, str]], ) -> bool: _, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) bcmd = [sfsenc(x) for x in acmd] diff --git a/copyparty/web/rups.js b/copyparty/web/rups.js index 17a44556..e4e8b3ce 100644 --- a/copyparty/web/rups.js +++ b/copyparty/web/rups.js @@ -1,5 +1,5 @@ function render() { - var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>']; + var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>ip</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>']; var ups = V.ups, now = V.now; ebi('filter').value = V.filter; ebi('hits').innerHTML = 'showing ' + ups.length + ' files'; @@ -16,6 +16,7 @@ function render() { sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " "); html.push('<tr><td>' + sz + + '</td><td>' + (f.un || '') + '</td><td>' + f.ip + '</td><td>' + ts + '</td><td>' + sa + From e3c7d6776ee401673a9c34e4fd8a6e6fb2e86dad Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 15 Aug 2025 21:34:56 +0000 Subject: [PATCH 047/154] fix test --- tests/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index 16a0132a..14031829 100644 --- a/tests/util.py +++ b/tests/util.py @@ -155,7 +155,7 @@ class Cfg(Namespace): ex = "gid uid" ka.update(**{k: -1 for k in ex.split()}) - ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz" + ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz unp_who" ka.update(**{k: 1 for k in ex.split()}) ex = "ac_convt au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" From 23ea1c8a14ae2c8adb7a38bc5f51bb42bbaa636b Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 15 Aug 2025 21:35:10 +0000 Subject: [PATCH 048/154] better dropdown color --- copyparty/web/browser.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 00e42cae..f5f472a5 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1377,7 +1377,7 @@ html.y #ops svg circle { .opview select, .opview input[type=text] { color: var(--fg); - background: var(--bg-u5); + background: var(--txt-bg); border: none; box-shadow: 0 0 2px var(--txt-sh); border-bottom: 1px solid #999; @@ -1388,6 +1388,7 @@ html.y #ops svg circle { .opview select { padding: .3em; margin: .2em .4em; + background: var(--bg-u3); } .opview input.err { color: var(--err-fg); From 43a19779c181eeb22734e2417b662e667d1a0ad7 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 15 Aug 2025 21:36:02 +0000 Subject: [PATCH 049/154] ftp: fix potential utime issue; closes #539 --- copyparty/ftpd.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 14f8d5b6..b7246291 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -409,8 +409,12 @@ class FtpFs(AbstractedFS): return st def utime(self, path: str, timeval: float) -> None: - ap = self.rv2a(path, w=True)[0] - return bos.utime(ap, (timeval, timeval)) + try: + ap = self.rv2a(path, w=True)[0] + return bos.utime(ap, (int(time.time()), int(timeval))) + except Exception as ex: + logging.error("ftp.utime: %s, %r", ex, ex) + raise def lstat(self, path: str) -> os.stat_result: ap = self.rv2a(path)[0] From 187cae25bf3d42c516d3fb679573651108a0cb48 Mon Sep 17 00:00:00 2001 From: Toast <39011842+toast003@users.noreply.github.com> Date: Sat, 16 Aug 2025 13:29:03 +0900 Subject: [PATCH 050/154] Build nix packages from source (#253) * nix: get source tarball with update.py * nix: build from source nix: remove u2c and partyfuse packages The main copyparty package has u2c and partyfuse, so these packages are redundant now nix: add fusepy dependency fix: nix: use replace pyfuse with fusepy * nix: fix extra python packages * nix: add optional dependencies * nix: add partftpy package * nix: add tftp parameter to package * nix: enable pyproject for partftpy package * nix: replace partftpy overlay with real package * nix: add updater for partftpy * nix: bring back local release pin to update.py nix: update local release pin function in update.py --------- Signed-off-by: Toast <39011842+toast003@users.noreply.github.com> --- contrib/package/nix/copyparty/default.nix | 60 +++++++++++++---------- contrib/package/nix/copyparty/pin.json | 4 +- contrib/package/nix/copyparty/update.py | 23 ++++----- contrib/package/nix/partftpy/default.nix | 30 ++++++++++++ contrib/package/nix/partftpy/pin.json | 5 ++ contrib/package/nix/partftpy/update.py | 50 +++++++++++++++++++ contrib/package/nix/partyfuse/default.nix | 26 ---------- contrib/package/nix/u2c/default.nix | 24 --------- flake.nix | 15 ++---- 9 files changed, 139 insertions(+), 98 deletions(-) create mode 100644 contrib/package/nix/partftpy/default.nix create mode 100644 contrib/package/nix/partftpy/pin.json create mode 100755 contrib/package/nix/partftpy/update.py delete mode 100644 contrib/package/nix/partyfuse/default.nix delete mode 100644 contrib/package/nix/u2c/default.nix diff --git a/contrib/package/nix/copyparty/default.nix b/contrib/package/nix/copyparty/default.nix index 28d733ab..22ae5bcd 100644 --- a/contrib/package/nix/copyparty/default.nix +++ b/contrib/package/nix/copyparty/default.nix @@ -1,10 +1,10 @@ { lib, - stdenv, - makeWrapper, + buildPythonApplication, fetchurl, util-linux, python, + setuptools, jinja2, impacket, pyopenssl, @@ -15,6 +15,10 @@ pyzmq, ffmpeg, mutagen, + pyftpdlib, + magic, + partftpy, + fusepy, # for partyfuse # use argon2id-hashed passwords in config files (sha2 is always available) withHashedPasswords ? true, @@ -40,12 +44,21 @@ # send ZeroMQ messages from event-hooks withZeroMQ ? true, + # enable FTP server + withFTP ? true, + # enable FTPS support in the FTP server withFTPS ? false, + # enable TFTP server + withTFTP ? false, + # samba/cifs server; dangerous and buggy, enable if you really need it withSMB ? false, + # enables filetype detection for nameless uploads + withMagic ? false, + # extra packages to add to the PATH extraPackages ? [ ], @@ -58,14 +71,23 @@ let pinData = lib.importJSON ./pin.json; - pyEnv = python.withPackages ( - ps: - with ps; + runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg); +in +buildPythonApplication { + pname = "copyparty"; + inherit (pinData) version; + src = fetchurl { + inherit (pinData) url hash; + }; + dependencies = [ jinja2 + fusepy ] ++ lib.optional withSMB impacket + ++ lib.optional withFTP pyftpdlib ++ lib.optional withFTPS pyopenssl + ++ lib.optional withTFTP partftpy ++ lib.optional withCertgen cfssl ++ lib.optional withThumbnails pillow ++ lib.optional withFastThumbnails pyvips @@ -73,25 +95,14 @@ let ++ lib.optional withBasicAudioMetadata mutagen ++ lib.optional withHashedPasswords argon2-cffi ++ lib.optional withZeroMQ pyzmq - ++ (extraPythonPackages ps) - ); + ++ lib.optional withMagic magic + ++ (extraPythonPackages python.pkgs); + makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ]; - runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg); -in -stdenv.mkDerivation { - pname = "copyparty"; - inherit (pinData) version; - src = fetchurl { - inherit (pinData) url hash; - }; - nativeBuildInputs = [ makeWrapper ]; - dontUnpack = true; - installPhase = '' - install -Dm755 $src $out/share/copyparty-sfx.py - makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \ - --prefix PATH : ${lib.makeBinPath runtimeDeps} \ - --add-flag $out/share/copyparty-sfx.py - ''; + pyproject = true; + build-system = [ + setuptools + ]; meta = { description = "Turn almost any device into a file server"; longDescription = '' @@ -101,8 +112,7 @@ stdenv.mkDerivation { homepage = "https://github.com/9001/copyparty"; changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}"; license = lib.licenses.mit; - inherit (python.meta) platforms; mainProgram = "copyparty"; - sourceProvenance = [ lib.sourceTypes.binaryBytecode ]; + sourceProvenance = [ lib.sourceTypes.fromSource ]; }; } diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 55f1c2e1..2651d12e 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.19.1/copyparty-sfx.py", + "url": "https://github.com/9001/copyparty/releases/download/v1.19.1/copyparty-1.19.1.tar.gz", "version": "1.19.1", - "hash": "sha256-Orn0N//DD5/5rIWK9yYRcvyOnt/bKCE9CeoxkfNO76s=" + "hash": "sha256-u8JQ2yPrgLyWwnsu+kVs4ef0nH36q625GlcfcAZLb5E=" } \ No newline at end of file diff --git a/contrib/package/nix/copyparty/update.py b/contrib/package/nix/copyparty/update.py index 363773c0..90542d2e 100755 --- a/contrib/package/nix/copyparty/update.py +++ b/contrib/package/nix/copyparty/update.py @@ -11,14 +11,14 @@ import base64 import json import hashlib import sys -import re +import tarfile from pathlib import Path OUTPUT_FILE = Path("pin.json") -TARGET_ASSET = "copyparty-sfx.py" +TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz" HASH_TYPE = "sha256" LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest" -DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}" +DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}" def get_formatted_hash(binary): @@ -29,11 +29,13 @@ def get_formatted_hash(binary): return f"{HASH_TYPE}-{encoded_hash}" -def version_from_sfx(binary): - result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE) - if result: - return result.groups(1)[0].decode("ascii") +def version_from_tar_gz(path): + with tarfile.open(path) as tarball: + release_name = tarball.getmembers()[0].name + prefix = "copyparty-" + if release_name.startswith(prefix): + return release_name.replace(prefix, "") raise ValueError("version not found in provided file") @@ -42,7 +44,7 @@ def remote_release_pin(): response = requests.get(LATEST_RELEASE_URL).json() version = response["tag_name"].lstrip("v") - asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0] + asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0] download_url = asset_info["browser_download_url"] asset = requests.get(download_url) formatted_hash = get_formatted_hash(asset.content) @@ -52,10 +54,9 @@ def remote_release_pin(): def local_release_pin(path): - asset = path.read_bytes() - version = version_from_sfx(asset) + version = version_from_tar_gz(path) download_url = DOWNLOAD_URL(version) - formatted_hash = get_formatted_hash(asset) + formatted_hash = get_formatted_hash(path.read_bytes()) result = {"url": download_url, "version": version, "hash": formatted_hash} return result diff --git a/contrib/package/nix/partftpy/default.nix b/contrib/package/nix/partftpy/default.nix new file mode 100644 index 00000000..8293b44a --- /dev/null +++ b/contrib/package/nix/partftpy/default.nix @@ -0,0 +1,30 @@ +{ + lib, + buildPythonPackage, + fetchurl, + setuptools, +}: +let + pinData = lib.importJSON ./pin.json; +in + +buildPythonPackage rec { + pname = "partftpy"; + inherit (pinData) version; + pyproject = true; + + src = fetchurl { + inherit (pinData) url hash; + }; + + build-system = [ setuptools ]; + + pythonImportsCheck = [ "partftpy.TftpServer" ]; + + meta = { + description = "Pure Python TFTP library (copyparty edition)"; + homepage = "https://github.com/9001/partftpy"; + changelog = "https://github.com/9001/partftpy/releases/tag/${version}"; + license = lib.licenses.mit; + }; +} diff --git a/contrib/package/nix/partftpy/pin.json b/contrib/package/nix/partftpy/pin.json new file mode 100644 index 00000000..99882f05 --- /dev/null +++ b/contrib/package/nix/partftpy/pin.json @@ -0,0 +1,5 @@ +{ + "url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz", + "version": "0.4.0", + "hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0=" +} \ No newline at end of file diff --git a/contrib/package/nix/partftpy/update.py b/contrib/package/nix/partftpy/update.py new file mode 100755 index 00000000..01f046b5 --- /dev/null +++ b/contrib/package/nix/partftpy/update.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# Update the Nix package pin +# +# Usage: ./update.sh + +import base64 +import json +import hashlib +import sys +from pathlib import Path + +OUTPUT_FILE = Path("pin.json") +TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz" +HASH_TYPE = "sha256" +LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest" + + +def get_formatted_hash(binary): + hasher = hashlib.new("sha256") + hasher.update(binary) + asset_hash = hasher.digest() + encoded_hash = base64.b64encode(asset_hash).decode("ascii") + return f"{HASH_TYPE}-{encoded_hash}" + + +def remote_release_pin(): + import requests + + response = requests.get(LATEST_RELEASE_URL).json() + version = response["tag_name"].lstrip("v") + asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0] + download_url = asset_info["browser_download_url"] + asset = requests.get(download_url) + formatted_hash = get_formatted_hash(asset.content) + + result = {"url": download_url, "version": version, "hash": formatted_hash} + return result + + +def main(): + result = remote_release_pin() + + print(result) + json_result = json.dumps(result, indent=4) + OUTPUT_FILE.write_text(json_result) + + +if __name__ == "__main__": + main() diff --git a/contrib/package/nix/partyfuse/default.nix b/contrib/package/nix/partyfuse/default.nix deleted file mode 100644 index 59faa914..00000000 --- a/contrib/package/nix/partyfuse/default.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ - stdenvNoCC, - copyparty, - python3, - makeBinaryWrapper, -}: -let - python = python3.withPackages (p: [ p.fusepy ]); -in -stdenvNoCC.mkDerivation { - pname = "partyfuse"; - inherit (copyparty) version meta; - src = ../../../..; - - nativeBuildInputs = [ makeBinaryWrapper ]; - - installPhase = '' - runHook preInstall - - install -Dm444 bin/partyfuse.py -t $out/share/copyparty - makeWrapper ${python.interpreter} $out/bin/partyfuse \ - --add-flag $out/share/copyparty/partyfuse.py - - runHook postInstall - ''; -} diff --git a/contrib/package/nix/u2c/default.nix b/contrib/package/nix/u2c/default.nix deleted file mode 100644 index dc1e4c56..00000000 --- a/contrib/package/nix/u2c/default.nix +++ /dev/null @@ -1,24 +0,0 @@ -{ - stdenvNoCC, - copyparty, - python312, - makeBinaryWrapper, -}: -stdenvNoCC.mkDerivation { - pname = "u2c"; - inherit (copyparty) version meta; - src = ../../../..; - - nativeBuildInputs = [ makeBinaryWrapper ]; - - installPhase = '' - runHook preInstall - - install -Dm444 bin/u2c.py -t $out/share/copyparty - mkdir $out/bin - makeWrapper ${python312.interpreter} $out/bin/u2c \ - --add-flag $out/share/copyparty/u2c.py - - runHook postInstall - ''; -} diff --git a/flake.nix b/flake.nix index 5f39d605..c5181bfe 100644 --- a/flake.nix +++ b/flake.nix @@ -12,17 +12,14 @@ }: { nixosModules.default = ./contrib/nixos/modules/copyparty.nix; - overlays.default = final: prev: rec { + overlays.default = final: prev: { copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty { ffmpeg = final.ffmpeg-full; }; - - partyfuse = prev.callPackage ./contrib/package/nix/partyfuse { - inherit copyparty; - }; - - u2c = prev.callPackage ./contrib/package/nix/u2c { - inherit copyparty; + python3 = prev.python3.override { + packageOverrides = pyFinal: pyPrev: { + partftpy = pyFinal.callPackage ./contrib/package/nix/partftpy { }; + }; }; }; } @@ -54,8 +51,6 @@ packages = { inherit (pkgs) copyparty - partyfuse - u2c ; default = self.packages.${system}.copyparty; }; From 274c074775f26af734cd6ae2e50c1ee175089df7 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 16 Aug 2025 20:45:46 +0000 Subject: [PATCH 051/154] hide --rp-loc in tree; closes #306 --- copyparty/web/browser.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 92bba2ba..2794624b 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -15108,7 +15108,8 @@ var showfile = (function () { }; r.mktree = function () { - var crumbs = linksplit(get_evpath()).join('<span>/</span>'), + var top = get_evpath().slice(SR.length), + crumbs = linksplit(top).join('<span>/</span>'), html = ['<li class="bn">' + L.tv_lst + '<br />' + crumbs + '</li>']; for (var a = 0; a < r.files.length; a++) { var file = r.files[a]; @@ -16846,13 +16847,22 @@ var treectl = (function () { } ebi('treeul').setAttribute('ts', ts); + if (SR && !top0) { + var x = SR.slice(1).split('/'); + while (x[0]) { + res = res['k' + x.shift()]; + if (!res) + throw 'invalid --rp-loc (or bug?)'; + } + } + var top = (top0 == '.' ? dst : top0).split('?')[0], name = uricom_dec(top.split('/').slice(-2)[0]), rtop = top.replace(/^\/+/, ""), - html = parsetree(res, rtop); + html = parsetree(res, rtop.slice(SR.length)); if (!top0) { - html = '<li><a href="#">-</a><a href="/">[root]</a>\n<ul>' + html; + html = '<li><a href="#">-</a><a href="' + SR + '/">[root]</a>\n<ul>' + html; if (rst || !ebi('treeul').getElementsByTagName('li').length) ebi('treeul').innerHTML = html + '</ul></li>'; } @@ -16995,10 +17005,6 @@ var treectl = (function () { return; } var href = this.getAttribute('href'); - if (R && !href.startsWith(SR)) { - location = href; - return; - } r.reqls(href, true); r.dir_cb = tree_scrollto; thegrid.setvis(true); @@ -17466,7 +17472,7 @@ var treectl = (function () { url = '/' + (top ? top + uek : uek) + '/', sym = res[kk] ? '-' : '+', link = '<a href="#">' + sym + '</a><a href="' + - url + kdk + '">' + hek + '</a>'; + SR + url + kdk + '">' + hek + '</a>'; if (res[kk]) { var subtree = parsetree(res[kk], url.slice(1)); From dcc6b1b4eff42eca34da0fdeedf1fa360aa4701f Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 16 Aug 2025 21:54:45 +0000 Subject: [PATCH 052/154] fix download-selection in old firefox; closes #618 --- copyparty/web/util.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/copyparty/web/util.js b/copyparty/web/util.js index 489cd416..b3a3c145 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -1263,10 +1263,13 @@ function sethash(hv) { function dl_file(url) { console.log('DL [%s]', url); - var o = mknod('a'); + qsr('#dlfth'); + var o = mknod('a', 'dlfth'); o.setAttribute('href', url); o.setAttribute('download', ''); - o.click(); + document.body.appendChild(o); + ebi('dlfth').click(); + qsr('#dlfth'); } From d9046f7e01ae182fcb827c6628b5f2cfe2ee462d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 16 Aug 2025 21:55:51 +0000 Subject: [PATCH 053/154] fix xvol false-positive; given the following config: * volume /a mapped to /srv/nas/ * volume /b mapped to /srv/nas/foo/ * anyone can read volume /a but not /b accessing /a/foo/ would incorrectly fail because the xvol-check would select /b based on its abspath being physically closer, not considering that the same abspath is reachable from /a --- copyparty/authsrv.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 5329ede1..f5f5c223 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -881,6 +881,15 @@ class VFS(object): return None if "xvol" in self.flags: + self_ap = self.realpath + os.sep + if aps.startswith(self_ap): + vp = aps[len(self_ap) :] + if ANYWIN: + vp = vp.replace(os.sep, "/") + vn2, _ = self._find(vp) + if self == vn2: + return self + all_aps = self.shr_all_aps or self.root.all_aps for vap, vns in all_aps: From 98d117b8adf9377173ca31b3c24629197f9bdb39 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 16 Aug 2025 23:00:15 +0000 Subject: [PATCH 054/154] music-thumbs: use embedded art as default (closes #252); previous behavior can be restored with --th-spec-p 2 thumbnails cache (.hist/th/) must be deleted to take effect --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 2 +- copyparty/cfg.py | 2 ++ copyparty/mtag.py | 21 +++++++++++++------ copyparty/th_srv.py | 47 +++++++++++++++++++++++++++++++++++-------- tests/util.py | 2 +- 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 01216722..6b23ae19 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1514,6 +1514,7 @@ def add_thumbnail(ap): ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") + ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec") # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://github.com/libvips/libvips # https://stackoverflow.com/a/47612661 diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index f5f5c223..0c8a5ab4 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2227,7 +2227,7 @@ class AuthSrv(object): if vf not in vol.flags: vol.flags[vf] = getattr(self.args, ga) - zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who" + zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who" for k in zs.split(): if k in vol.flags: vol.flags[k] = int(vol.flags[k]) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 3a7a205b..4fa73e72 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -112,6 +112,7 @@ def vf_vmap() -> dict[str, str]: "tail_tmax", "tail_who", "tcolor", + "th_spec_p", "txt_eol", "unlist", "u2abort", @@ -264,6 +265,7 @@ flagcats = { "th3x": "3x resolution (y/n/fy/fn)", "convt": "convert-to-image timeout in seconds", "aconvt": "convert-to-audio timeout in seconds", + "th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always", "ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s", }, "handlers\n(better explained in --help-handlers)": { diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 43e9dd75..29750733 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -208,7 +208,7 @@ def au_unpk( def ffprobe( abspath: str, timeout: int = 60 -) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]: +) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: cmd = [ b"ffprobe", b"-hide_banner", @@ -222,8 +222,17 @@ def ffprobe( return parse_ffprobe(so) -def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]: - """ffprobe -show_format -show_streams""" +def parse_ffprobe( + txt: str, +) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: + """ + txt: output from ffprobe -show_format -show_streams + returns: + * normalized tags + * original/raw tags + * list of streams + * format props + """ streams = [] fmt = {} g = {} @@ -316,7 +325,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ ret[rk] = v1 if ret.get("vc") == "ansi": # shellscript - return {}, {} + return {}, {}, [], {} for strm in streams: for sk, sv in strm.items(): @@ -365,7 +374,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ zero = int("0") zd = {k: (zero, v) for k, v in ret.items()} - return zd, md + return zd, md, streams, fmt def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]: @@ -706,7 +715,7 @@ class MTag(object): if not bos.path.isfile(abspath): return {} - ret, md = ffprobe(abspath, self.args.mtag_to) + ret, md, _, _ = ffprobe(abspath, self.args.mtag_to) if self.args.mtag_vv: for zd in (ret, dict(md)): diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 2522f3a6..86c3c469 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -612,7 +612,7 @@ class ThumbSrv(object): def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) - ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) + ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if not ret: return @@ -623,6 +623,17 @@ class ThumbSrv(object): dur = ret[".dur"][1] if ".dur" in ret else 4 seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] + self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0") + + def _ffmpeg_im( + self, + abspath: str, + tpath: str, + fmt: str, + vn: VFS, + seek: list[bytes], + imap: bytes, + ) -> None: scale = "scale={0}:{1}:force_original_aspect_ratio=" if "f" in fmt: scale += "decrease,setsar=1:1" @@ -641,7 +652,7 @@ class ThumbSrv(object): cmd += seek cmd += [ b"-i", fsenc(abspath), - b"-map", b"0:v:0", + b"-map", imap, b"-vf", bscale, b"-frames:v", b"1", b"-metadata:s:v:0", b"rotate=0", @@ -710,7 +721,7 @@ class ThumbSrv(object): raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: - ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) + ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in ret: raise Exception("not audio") @@ -769,11 +780,31 @@ class ThumbSrv(object): else: atomic_move(self.log, wtpath, tpath, vn.flags) + def conv_emb_cv( + self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any] + ) -> None: + self.wait4ram(0.2, tpath) + self._ffmpeg_im( + abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii") + ) + def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: - ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) + ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in ret: raise Exception("not audio") + want_spec = vn.flags.get("th_spec_p", 1) + if want_spec < 2: + for strm in strms: + if ( + strm.get("codec_type") == "video" + and strm.get("DISPOSITION:attached_pic") == "1" + ): + return self.conv_emb_cv(abspath, tpath, fmt, vn, strm) + + if not want_spec: + raise Exception("spectrograms forbidden by volflag") + fext = abspath.split(".")[-1].lower() # https://trac.ffmpeg.org/ticket/10797 @@ -859,7 +890,7 @@ class ThumbSrv(object): raise Exception("disabled in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") @@ -897,7 +928,7 @@ class ThumbSrv(object): raise Exception("flac not permitted in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") @@ -922,7 +953,7 @@ class ThumbSrv(object): raise Exception("wav not permitted in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") @@ -957,7 +988,7 @@ class ThumbSrv(object): raise Exception("disabled in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") diff --git a/tests/util.py b/tests/util.py index 14031829..546851b4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -155,7 +155,7 @@ class Cfg(Namespace): ex = "gid uid" ka.update(**{k: -1 for k in ex.split()}) - ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz unp_who" + ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate th_spec_p u2abort u2j u2sz unp_who" ka.update(**{k: 1 for k in ex.split()}) ex = "ac_convt au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" From d4cf42e760bf5f12382ba89436928064e6c4eb49 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 09:06:23 +0000 Subject: [PATCH 055/154] show severity in logs with no-ansi; #616 --- copyparty/svchub.py | 9 ++++++++- docs/examples/docker/basic-docker-compose/copyparty.conf | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 8661bc29..92a797d4 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1464,7 +1464,14 @@ class SvcHub(object): fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" if self.no_ansi: - fmt = "%s %-21s %s\n" + if c == 1: + fmt = "%s %-21s CRIT: %s\n" + elif c == 3: + fmt = "%s %-21s WARN: %s\n" + elif c == 6: + 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: diff --git a/docs/examples/docker/basic-docker-compose/copyparty.conf b/docs/examples/docker/basic-docker-compose/copyparty.conf index 59820103..ba751549 100644 --- a/docs/examples/docker/basic-docker-compose/copyparty.conf +++ b/docs/examples/docker/basic-docker-compose/copyparty.conf @@ -6,7 +6,7 @@ [global] e2dsa # enable file indexing and filesystem scanning e2ts # enable multimedia indexing - ansi # enable colors in log messages + ansi # enable colors in log messages (both in logfiles and stdout) # q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker From f4727f8ea397a4ccbdfe367ee320871c49e99fa2 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 10:05:25 +0000 Subject: [PATCH 056/154] fix config expansion order; closes #556 --- copyparty/__main__.py | 49 ++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 6b23ae19..905876fc 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -436,6 +436,40 @@ def args_from_cfg(cfg_path: str) -> list[str]: return ret +def expand_cfg(argv) -> list[str]: + if CFG_DEF: + supp = args_from_cfg(CFG_DEF[0]) + argv = supp + argv + + n = spins = 0 + while n < len(argv): + if not n: + if spins % 1000 == 999: + t = "still expanding config files... giving up after %d more" + print(t % (9999 - spins)) + if spins > 9999: + t = "got stuck expanding config files; do you have a config-file which imports itself? this is where I gave up:\n%r" + raise Exception(t % (argv[:1000])) + v1 = argv[n] + v1v = v1[2:].lstrip("=") + try: + v2 = argv[n + 1] + except: + v2 = "" + + if v1 == "-c" and v2 and os.path.isfile(v2): + argv = argv[:n] + args_from_cfg(v2) + argv[n + 2 :] + spins += 1 + n = 0 + elif v1.startswith("-c") and v1v and os.path.isfile(v1v): + argv = argv[:n] + args_from_cfg(v1v) + argv[n + 1 :] + spins += 1 + n = 0 + else: + n += 1 + return argv + + def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None: msg = [""] * 5 for th in threading.enumerate(): @@ -1845,20 +1879,7 @@ def main(argv: Optional[list[str]] = None) -> None: ensure_webdeps() - if CFG_DEF: - supp = args_from_cfg(CFG_DEF[0]) - argv.extend(supp) - - for k, v in zip(argv[1:], argv[2:]): - if k == "-c" and os.path.isfile(v): - supp = args_from_cfg(v) - argv.extend(supp) - - for k in argv[1:]: - v = k[2:] - if k.startswith("-c") and v and os.path.isfile(v): - supp = args_from_cfg(v) - argv.extend(supp) + argv = expand_cfg(argv) deprecated: list[tuple[str, str]] = [ ("--salt", "--warksalt"), From 782e2f1de305bf06944b87eace8ad2e6429256bb Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 10:42:45 +0000 Subject: [PATCH 057/154] bbox: stay fullscreen --- copyparty/web/baguettebox.js | 3 ++- copyparty/web/browser.css | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/copyparty/web/baguettebox.js b/copyparty/web/baguettebox.js index adcf3239..9a298e7b 100644 --- a/copyparty/web/baguettebox.js +++ b/copyparty/web/baguettebox.js @@ -48,6 +48,7 @@ window.baguetteBox = (function () { var onFSC = function (e) { isFullscreen = !!document.fullscreenElement; + clmod(document.documentElement, 'bb_fsc', isFullscreen); }; var overlayClickHandler = function (e) { @@ -402,7 +403,7 @@ window.baguetteBox = (function () { if (isFullscreen) document.exitFullscreen(); else - (vid() || ebi('bbox-overlay')).requestFullscreen(); + ebi('bbox-overlay').requestFullscreen(); } catch (ex) { if (IPHONE) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index f5f472a5..8b87d4c2 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -2126,6 +2126,13 @@ html.noscroll .sbar::-webkit-scrollbar { vertical-align: middle; transition: transform .23s, left .23s, top .23s, width .23s, height .23s; } +html.bb_fsc .full-image img, +html.bb_fsc .full-image video { + max-height: 100%; +} +html.bb_fsc figcaption { + display: none; +} .full-image img.nt, .full-image video.nt { transition: none; From 55c85d098437c0a6dfc18ec648706e665c3e51e0 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 11:06:38 +0000 Subject: [PATCH 058/154] docker: add rawpy to iv/dj --- scripts/docker/Dockerfile.dj | 3 +++ scripts/docker/Dockerfile.iv | 3 +++ 2 files changed, 6 insertions(+) diff --git a/scripts/docker/Dockerfile.dj b/scripts/docker/Dockerfile.dj index 8302a2cb..cdf9f28b 100644 --- a/scripts/docker/Dockerfile.dj +++ b/scripts/docker/Dockerfile.dj @@ -19,13 +19,16 @@ RUN apk add -U !pyc \ vips-jxl vips-heif vips-poppler vips-magick \ py3-numpy fftw libsndfile \ vamp-sdk vamp-sdk-libs \ + libraw py3-numpy cython \ && apk add -t .bd \ bash wget gcc g++ make cmake patchelf \ python3-dev ffmpeg-dev fftw-dev libsndfile-dev \ py3-wheel py3-numpy-dev libffi-dev \ vamp-sdk-dev \ + libraw-dev py3-numpy-dev \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && python3 -m pip install pyvips \ + && python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \ && bash install-deps.sh \ && apk del py3-pip .bd \ && chmod 777 /root \ diff --git a/scripts/docker/Dockerfile.iv b/scripts/docker/Dockerfile.iv index d639d3df..a0fe92ed 100644 --- a/scripts/docker/Dockerfile.iv +++ b/scripts/docker/Dockerfile.iv @@ -14,11 +14,14 @@ RUN apk add -U !pyc \ ffmpeg \ py3-magic \ vips-jxl vips-heif vips-poppler vips-magick \ + libraw py3-numpy cython \ && apk add -t .bd \ bash wget gcc g++ make cmake patchelf \ python3-dev py3-wheel libffi-dev \ + libraw-dev py3-numpy-dev \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && python3 -m pip install pyvips \ + && python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \ && apk del py3-pip .bd COPY i/dist/copyparty-sfx.py innvikler.sh ./ From 96cb5abf5355d2bbdb264df43b788c8758bdc84e Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 11:07:02 +0000 Subject: [PATCH 059/154] extend vips heif formats --- copyparty/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 905876fc..27fd6541 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1554,7 +1554,7 @@ def add_thumbnail(ap): # https://stackoverflow.com/a/47612661 # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") - ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") + ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") From a113d3b92512f746f0e6283cf209c19944abcc10 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 11:21:25 +0000 Subject: [PATCH 060/154] v1.19.2 --- copyparty/__main__.py | 5 +++-- copyparty/__version__.py | 4 ++-- docs/changelog.md | 23 +++++++++++++++++++++++ scripts/pyinstaller/deps.sha512 | 2 +- scripts/pyinstaller/notes.txt | 2 +- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 27fd6541..8bf042a4 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1672,13 +1672,14 @@ def add_og(ap): def add_ui(ap, retry): + THEMES = 10 ap2 = ap.add_argument_group("ui options") ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)") 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("--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..7)") - ap2.add_argument("--themes", metavar="NUM", type=int, default=10, help="number of themes installed") + 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") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)") ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)") diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 1cea26c5..2a64530f 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 1) +VERSION = (1, 19, 2) CODENAME = "usernames" -BUILD_DT = (2025, 8, 10) +BUILD_DT = (2025, 8, 17) 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 91fd1b84..a1dfa881 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,26 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-0810-1226 `v1.19.1` archlinux fix + +## 🧪 new features + +* new translations: + * #486 French (thx @Tr3yWay996, @Packingdustry, @Alee14, @jakubiakfr, @Equinoxs!) e9ddfccf 7aa21483 b87f8f1b + * #463 Polish (thx @pufereq and @daimond113!) 392a4db5 + * #537 Nynorsk (thx @chinatsu!) 3931bc27 +* #549 custom mdns domain 3c78c6a8 + +## 🩹 bugfixes + +* #539 FTP glitches when running on windows 8ba98877 +* #555 global-config didn't load through PRTY_CONFIG (thx @icxes!) 074e106e +* macos: could take a while to establish webdav connection from finder a01870b7 +* ux: + * dropdown colors 347cf6a5 + * case-sensitivity in filters e5e82295 + * iOS being too enthusiastic about using saved passwords 03acd65e + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-0807-2213 `v1.19.0` usernames diff --git a/scripts/pyinstaller/deps.sha512 b/scripts/pyinstaller/deps.sha512 index 1d324c0e..5aa63686 100644 --- a/scripts/pyinstaller/deps.sha512 +++ b/scripts/pyinstaller/deps.sha512 @@ -30,5 +30,5 @@ a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac7 3e39ea6e16b502d99a2e6544579095d0f7c6097761cd85135d5e929b9dec1b32e80669a846f94ee8c2cca9be2f5fe728625d09453988864c04e16bb8445c3f91 pillow-11.3.0-cp313-cp313-win_amd64.whl 59fbbcae044f4ee73d203ac74b553b27bfad3e6b2f3fb290fd3f8774753c6b545176b6b3399c240b092d131d152290ce732750accd962dc1e48e930be85f5e53 pyinstaller-6.14.1-py3-none-win_amd64.whl fc6f3e144c5f5b662412de07cb8bf0c2eb3b3be21d19ec448aef3c4244d779b9ab8027fd67a4871e6e13823b248ea0f5a7a9241a53aef30f3b51a6d3cb5bdb3f pyinstaller_hooks_contrib-2025.5-py3-none-any.whl -3c37ea72ab062f65116a5faaaccbaa960a971797c78dbe6a504f41e562b018e7f28924647b5577fdb4fedfa61ffe0f1153842a8585f93b0332ed4d97905f6609 python-3.13.6-amd64.exe +36db028e9f3d6805a57e89320283c07bd5eb0bb15c6edcd2ae4a7e46b06bfe6c96ed0793e8936cbb09b4f6b680a3f06dace2220a1e7d8b74ab6047698871db9e python-3.13.7-amd64.exe 2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl diff --git a/scripts/pyinstaller/notes.txt b/scripts/pyinstaller/notes.txt index 44cebd10..6cce39d0 100644 --- a/scripts/pyinstaller/notes.txt +++ b/scripts/pyinstaller/notes.txt @@ -40,7 +40,7 @@ fns=( pillow-11.3.0-cp313-cp313-win_amd64.whl pyinstaller-6.14.1-py3-none-win_amd64.whl pyinstaller_hooks_contrib-2025.5-py3-none-any.whl - python-3.13.6-amd64.exe + python-3.13.7-amd64.exe setuptools-80.9.0-py3-none-any.whl ) [ $w7 ] && fns+=( From e7f2c6d8068f00191a080b3dfc72ae63efc53b75 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 11:51:56 +0000 Subject: [PATCH 061/154] update pkgs to 1.19.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 3c6a4abb..d9f6b333 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.19.1" +pkgver="1.19.2" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("bbc250db23eb80bc96c27b2efa456ce1e7f49c7dfaabadb91a571f70064b6f91") +sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 6201430a..c4bdb888 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.1 +pkgver=1.19.2 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("bbc250db23eb80bc96c27b2efa456ce1e7f49c7dfaabadb91a571f70064b6f91") +sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 2651d12e..1025294b 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.19.1/copyparty-1.19.1.tar.gz", - "version": "1.19.1", - "hash": "sha256-u8JQ2yPrgLyWwnsu+kVs4ef0nH36q625GlcfcAZLb5E=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.2/copyparty-1.19.2.tar.gz", + "version": "1.19.2", + "hash": "sha256-nw3NgSTyYKDHJna3DYTII4jP5bR+fQVW9RkMiCCFgKI=" } \ No newline at end of file From bf1fdcab096e65c244a01dddde05921ffd6dce4a Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 15:19:25 +0000 Subject: [PATCH 062/154] fix #556 fuckup, fixes #624 --- copyparty/__main__.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 8bf042a4..5ee46622 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -441,15 +441,8 @@ def expand_cfg(argv) -> list[str]: supp = args_from_cfg(CFG_DEF[0]) argv = supp + argv - n = spins = 0 + n = 0 while n < len(argv): - if not n: - if spins % 1000 == 999: - t = "still expanding config files... giving up after %d more" - print(t % (9999 - spins)) - if spins > 9999: - t = "got stuck expanding config files; do you have a config-file which imports itself? this is where I gave up:\n%r" - raise Exception(t % (argv[:1000])) v1 = argv[n] v1v = v1[2:].lstrip("=") try: @@ -457,16 +450,12 @@ def expand_cfg(argv) -> list[str]: except: v2 = "" + n += 1 if v1 == "-c" and v2 and os.path.isfile(v2): - argv = argv[:n] + args_from_cfg(v2) + argv[n + 2 :] - spins += 1 - n = 0 - elif v1.startswith("-c") and v1v and os.path.isfile(v1v): - argv = argv[:n] + args_from_cfg(v1v) + argv[n + 1 :] - spins += 1 - n = 0 - else: n += 1 + argv = argv[:n] + args_from_cfg(v2) + argv[n:] + elif v1.startswith("-c") and v1v and os.path.isfile(v1v): + argv = argv[:n] + args_from_cfg(v1v) + argv[n:] return argv From da4ae661736ed6be9e18d53d1743d2afe77276e9 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 15:22:17 +0000 Subject: [PATCH 063/154] v1.19.3 --- copyparty/__version__.py | 2 +- scripts/make-sfx.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 2a64530f..f3ed7d21 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,6 +1,6 @@ # coding: utf-8 -VERSION = (1, 19, 2) +VERSION = (1, 19, 3) CODENAME = "usernames" BUILD_DT = (2025, 8, 17) diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index f752f8a8..c3552a63 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -217,7 +217,7 @@ necho() { tar -zxf $f mv pyftpdlib-*/pyftpdlib . rm -rf pyftpdlib-* pyftpdlib/test - patch -p1 <../scripts/patches/pyftpdlib-win313.patch + patch -s -p1 <../scripts/patches/pyftpdlib-win313.patch for f in pyftpdlib/_async{hat,ore}.py; do [ -e "$f" ] || continue; iawk 'NR<4||NR>27||!/^#/;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' $f From b2fb0c26add48e02ffb72859061d60a583a3b905 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 15:33:07 +0000 Subject: [PATCH 064/154] update pkgs to 1.19.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 d9f6b333..fd67b9cd 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.19.2" +pkgver="1.19.3" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2") +sha256sums=("789701b25d00d6d3d58e36801774acd3da08d95aeb7028f1ec1cc993668bab83") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index c4bdb888..407774d7 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.2 +pkgver=1.19.3 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2") +sha256sums=("789701b25d00d6d3d58e36801774acd3da08d95aeb7028f1ec1cc993668bab83") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 1025294b..380db50d 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.19.2/copyparty-1.19.2.tar.gz", - "version": "1.19.2", - "hash": "sha256-nw3NgSTyYKDHJna3DYTII4jP5bR+fQVW9RkMiCCFgKI=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.3/copyparty-1.19.3.tar.gz", + "version": "1.19.3", + "hash": "sha256-eJcBsl0A1tPVjjaAF3Ss09oI2VrrcCjx7BzJk2aLq4M=" } \ No newline at end of file From 377f7732de159582c5cfcbc7b7914f26658142fe Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 15:56:56 +0000 Subject: [PATCH 065/154] v1.19.4 --- copyparty/__main__.py | 2 +- copyparty/__version__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 5ee46622..1a0ed743 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -439,7 +439,7 @@ def args_from_cfg(cfg_path: str) -> list[str]: def expand_cfg(argv) -> list[str]: if CFG_DEF: supp = args_from_cfg(CFG_DEF[0]) - argv = supp + argv + argv = argv[:1] + supp + argv[1:] n = 0 while n < len(argv): diff --git a/copyparty/__version__.py b/copyparty/__version__.py index f3ed7d21..639b85ba 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,6 +1,6 @@ # coding: utf-8 -VERSION = (1, 19, 3) +VERSION = (1, 19, 4) CODENAME = "usernames" BUILD_DT = (2025, 8, 17) From b5c6b4fa9972ed663e06d9bc73603c089f6f39a7 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 17 Aug 2025 16:11:06 +0000 Subject: [PATCH 066/154] update pkgs to 1.19.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 fd67b9cd..355d6cbc 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.19.3" +pkgver="1.19.4" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("789701b25d00d6d3d58e36801774acd3da08d95aeb7028f1ec1cc993668bab83") +sha256sums=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 407774d7..c3fb4186 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.3 +pkgver=1.19.4 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("789701b25d00d6d3d58e36801774acd3da08d95aeb7028f1ec1cc993668bab83") +sha256sums=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 380db50d..2aaef28d 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.19.3/copyparty-1.19.3.tar.gz", - "version": "1.19.3", - "hash": "sha256-eJcBsl0A1tPVjjaAF3Ss09oI2VrrcCjx7BzJk2aLq4M=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.4/copyparty-1.19.4.tar.gz", + "version": "1.19.4", + "hash": "sha256-sOhKeOsnAct0R7YCOvzsKAxVBhfd5ntvAoW7I0gxEes=" } \ No newline at end of file From 20ef74cdac7e3432a8ebf3015c065c763e1818c6 Mon Sep 17 00:00:00 2001 From: Ruby Iris Juric <ruby@srxl.me> Date: Tue, 19 Aug 2025 08:41:49 +1000 Subject: [PATCH 067/154] nix: make usage in non-flake setups easier (#296) * nix: extract overlay into own file * readme: document non-flake nixos usage --- README.md | 29 ++++++++++++++++++++++++++++- contrib/package/nix/overlay.nix | 11 +++++++++++ flake.nix | 11 +---------- 3 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 contrib/package/nix/overlay.nix diff --git a/README.md b/README.md index 1d143b6d..4754feb4 100644 --- a/README.md +++ b/README.md @@ -2344,7 +2344,7 @@ some recommended dependencies are enabled by default; [override the package](htt ## nixos module -for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of NixOS. +for [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS: ```nix { @@ -2371,6 +2371,33 @@ for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes) } ``` +if you don't use a flake in your configuration, you can use other dependency management tools like [npins](https://github.com/andir/npins), [niv](https://github.com/nmattia/niv), or even plain [`fetchTarball`](https://nix.dev/manual/nix/stable/language/builtins#builtins-fetchTarball), like so: + +```nix +{ pkgs, ... }: + +let + # npins example, adjust for your setup. copyparty should be a path to the downloaded repo + # for niv, just replace the npins folder import with the sources.nix file + copyparty = (import ./npins).copyparty; + + # or with fetchTarball: + copyparty = fetchTarball "https://github.com/9001/copyparty/archive/hovudstraum.tar.gz"; +in + +{ + # load the copyparty NixOS module + imports = [ "${copyparty}/contrib/nixos/modules/copyparty.nix" ]; + + # add the copyparty overlay to expose the package to the module + nixpkgs.overlays = [ "${copyparty}/contrib/package/nix/overlay.nix" ]; + # (optional) install the package globally + environment.systemPackages = [ pkgs.copyparty ]; + # configure the copyparty module + services.copyparty.enable = true; +} +``` + copyparty on NixOS is configured via `services.copyparty` options, for example: ```nix services.copyparty = { diff --git a/contrib/package/nix/overlay.nix b/contrib/package/nix/overlay.nix new file mode 100644 index 00000000..ca8d5c6e --- /dev/null +++ b/contrib/package/nix/overlay.nix @@ -0,0 +1,11 @@ +final: prev: { + copyparty = final.python3.pkgs.callPackage ./copyparty { + ffmpeg = final.ffmpeg-full; + }; + + python3 = prev.python3.override { + packageOverrides = pyFinal: pyPrev: { + partftpy = pyFinal.callPackage ./partftpy { }; + }; + }; +} diff --git a/flake.nix b/flake.nix index c5181bfe..336642ee 100644 --- a/flake.nix +++ b/flake.nix @@ -12,16 +12,7 @@ }: { nixosModules.default = ./contrib/nixos/modules/copyparty.nix; - overlays.default = final: prev: { - copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty { - ffmpeg = final.ffmpeg-full; - }; - python3 = prev.python3.override { - packageOverrides = pyFinal: pyPrev: { - partftpy = pyFinal.callPackage ./contrib/package/nix/partftpy { }; - }; - }; - }; + overlays.default = import ./contrib/package/nix/overlay.nix; } // flake-utils.lib.eachDefaultSystem ( system: From c51371c71d0449e3d3b223e7a3425f241065cae5 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Mon, 18 Aug 2025 23:08:49 +0000 Subject: [PATCH 068/154] mention syncthing compat (#490, #199) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4754feb4..532f181a 100644 --- a/README.md +++ b/README.md @@ -2584,6 +2584,8 @@ sync folders to/from copyparty NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/server/latest/user_manual/sv/files/desktop_mobile_sync.html) and [syncthing](https://syncthing.net/) does, will never be supported! Only single-direction sync (server-to-client, or client-to-server) is possible with copyparty +* if you want bidirectional sync, then copyparty and syncthing *should* be entirely safe to combine; they should be able to collaborate on the same folders without causing any trouble for eachother. Many people do this, and there have been no issues so far. But, if you *do* encounter any problems, please [file a copyparty bug](https://github.com/9001/copyparty/issues/new/choose) and I'll try to help -- just keep in mind I've never used syncthing before :-) + the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading) alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare From 5e36f025956dc9cc68a98c3039071f139353bbf7 Mon Sep 17 00:00:00 2001 From: DeStilleGast <3677706+DeStilleGast@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:44:05 +0200 Subject: [PATCH 069/154] Update browser.js - Found a typo Signed-off-by: DeStilleGast <3677706+DeStilleGast@users.noreply.github.com> --- copyparty/web/browser.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 2794624b..002be4d9 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -6472,12 +6472,12 @@ var Ls = { "ul_btnu": "U P L O A D", "ul_btns": "Z O E K E N", - "ul_hash": "Gash", - "ul_send": "Verstuur", + "ul_hash": "Hashing", + "ul_send": "Versturen", "ul_done": "Klaar", "ul_idle1": "Geen uploads in wachtrij", "ut_etah": "Gemiddelde <em>hashing</em> snelheid en geschatte tijd tot de voltooiing", - "ut_etau": "Gemiddelde <em>upload</em> snelheid en geschatte tijd tot voltooiing", + "ut_etau": "Gemiddelde <em>verzend</em> snelheid en geschatte tijd tot voltooiing", "ut_etat": "Gemiddelde <em>totale</em> snelheid en geschatte tijd tot voltooiing", "uct_ok": "Succesvol afgerond", From 3259367007197f408830c76dddc6f255a6793e6e Mon Sep 17 00:00:00 2001 From: Ruby Iris Juric <ruby@srxl.me> Date: Wed, 20 Aug 2025 11:36:24 +1000 Subject: [PATCH 070/154] readme: fix nixos overlay usage docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 532f181a..7c42b833 100644 --- a/README.md +++ b/README.md @@ -2390,7 +2390,7 @@ in imports = [ "${copyparty}/contrib/nixos/modules/copyparty.nix" ]; # add the copyparty overlay to expose the package to the module - nixpkgs.overlays = [ "${copyparty}/contrib/package/nix/overlay.nix" ]; + nixpkgs.overlays = [ (import "${copyparty}/contrib/package/nix/overlay.nix") ]; # (optional) install the package globally environment.systemPackages = [ pkgs.copyparty ]; # configure the copyparty module From 7a4973fa56f96994ebc9b6ed66728a885f9682d3 Mon Sep 17 00:00:00 2001 From: Massimo Melina <a@rejetto.com> Date: Wed, 20 Aug 2025 13:09:27 +0200 Subject: [PATCH 071/154] versus.md: corrections and updates for hfs3 Signed-off-by: Massimo Melina <a@rejetto.com> --- docs/versus.md | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/docs/versus.md b/docs/versus.md index 34d4d4b0..5a0abdd0 100644 --- a/docs/versus.md +++ b/docs/versus.md @@ -125,12 +125,12 @@ symbol legend, | config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ | | good documentation | | | | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ | | runs on iOS | ╱ | | | | | ╱ | | | | | | | | -| runs on Android | █ | | | | | █ | | | | | | | | +| runs on Android | █ | | █ | | | █ | | | | | | | | | runs on WinXP | █ | █ | | | | █ | | | | | | | | | runs on Windows | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | ╱ | | runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | -| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | | +| runs on FreeBSD | █ | | █ | • | █ | █ | █ | • | █ | █ | | █ | | | runs on Risc-V | █ | | | █ | █ | █ | | • | | █ | | | | | portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ | | zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | | ╱ | █ | @@ -161,7 +161,7 @@ symbol legend, | upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | | parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ | | resumable uploads | █ | | █ | | | | | | █ | █ | █ | ╱ | | -| upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | ╱ | █ | +| upload segmenting | █ | | █ | █ | | | | █ | █ | █ | █ | ╱ | █ | | upload acceleration | █ | | | | | | | | █ | | █ | | | | upload verification | █ | | | █ | █ | | | | █ | | | | | | upload deduplication | █ | | | | █ | | | | █ | | | | | @@ -169,7 +169,7 @@ symbol legend, | CTRL-V from device | █ | | | █ | | | | | | | | | | | race the beam ("p2p") | █ | | | | | | | | | | | | | | "tail -f" streaming | █ | | | | | | | | | | | | | -| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | | +| keep last-modified time | █ | | █ | █ | █ | █ | | | | | | █ | | | upload rules | ╱ | ╱ | ╱ | ╱ | ╱ | | | ╱ | ╱ | | ╱ | ╱ | ╱ | | ┗ max disk usage | █ | █ | █ | | █ | | | | █ | | | █ | █ | | ┗ max filesize | █ | | | | | | | █ | | | █ | █ | █ | @@ -251,7 +251,7 @@ symbol legend, | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | -| config from cmd args | █ | | | | | █ | █ | | | █ | | ╱ | ╱ | +| config from cmd args | █ | | █ | | | █ | █ | | | █ | | ╱ | ╱ | | config files | █ | █ | █ | ╱ | ╱ | █ | | █ | | █ | • | ╱ | ╱ | | runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ | | same-port http / https | █ | | | | | | | | | | | | | @@ -276,10 +276,10 @@ symbol legend, | per-account chroot | | | | | | | | | | | | █ | | | single-sign-on | ╱ | | | █ | █ | | | | • | | | | | | token auth | ╱ | | | █ | █ | | | █ | | | | | █ | -| 2fa | ╱ | | | █ | █ | | | | | | | █ | ╱ | +| 2fa | ╱ | | / | █ | █ | | | | | | | █ | ╱ | | per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | ╱ | █ | █ | -| per-folder permissions | ╱ | | | █ | █ | | █ | | █ | █ | ╱ | █ | █ | -| per-file permissions | | | | █ | █ | | █ | | █ | | | | █ | +| per-folder permissions | ╱ | | █ | █ | █ | | █ | | █ | █ | ╱ | █ | █ | +| per-file permissions | | | █ | █ | █ | | █ | | █ | | | | █ | | per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ | | unmap subfolders | █ | | █ | | | | █ | | | █ | ╱ | • | | | index.html blocks list | ╱ | | | | | | █ | | | • | | | | @@ -297,13 +297,13 @@ symbol legend, | full sync | | | | █ | █ | | | | | | | | | | speed throttle | | █ | █ | | | █ | | | █ | | | █ | | | anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ | • | -| dyndns updater | | █ | | | | | | | | | | | | +| dyndns updater | | █ | █ | | | | | | | | | | | | self-updater | | | █ | | | | | | | | | | █ | | log rotation | █ | | █ | █ | █ | | | • | █ | | | █ | • | | upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | ╱ | █ | | prometheus metrics | █ | | | █ | | | | | | | | █ | | | curl-friendly ls | █ | | | | | | | | | | | | | -| curl-friendly upload | █ | | | | | █ | █ | • | | | | | | +| curl-friendly upload | █ | | █ | | | █ | █ | • | | | | | | * `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path * `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that @@ -332,7 +332,8 @@ symbol legend, * `upload tracking / log` in main logfile * `m`/arozos: * `2fa` maybe possible through LDAP/Oauth - +* `c`/hfs3 + * `2fa` available by installing a plugin ## client features @@ -342,18 +343,18 @@ symbol legend, | themes | █ | █ | █ | █ | | | | | █ | | | | | | directory tree nav | █ | ╱ | | | █ | | | | █ | | ╱ | | | | multi-column sorting | █ | | | | | | | | | | | | | -| thumbnails | █ | | | ╱ | ╱ | | | █ | █ | ╱ | | | █ | -| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | | █ | +| thumbnails | █ | | / | ╱ | ╱ | | | █ | █ | ╱ | | | █ | +| ┗ image thumbnails | █ | | / | █ | █ | | | █ | █ | █ | | | █ | | ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | | █ | | ┗ audio spectrograms | █ | | | | | | | | | | | | | | audio player | █ | | ╱ | █ | █ | | | | █ | ╱ | | | █ | | ┗ gapless playback | █ | | | | | | | | • | | | | | | ┗ audio equalizer | █ | | | | | | | | | | | | | | ┗ waveform seekbar | █ | | | | | | | | | | | | | -| ┗ OS integration | █ | | | | | | | | | | | | | +| ┗ OS integration | █ | | █ | | | | | | | | | | | | ┗ transcode to lossy | █ | | | | | | | | | | | | | -| video player | █ | | | █ | █ | | | | █ | █ | | | █ | -| ┗ video transcoding | | | | | | | | | █ | | | | | +| video player | █ | | █ | █ | █ | | | | █ | █ | | | █ | +| ┗ video transcoding | | | / | | | | | | █ | | | | | | audio BPM detector | █ | | | | | | | | | | | | | | audio key detector | █ | | | | | | | | | | | | | | search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ | | | @@ -366,15 +367,15 @@ symbol legend, | undo recent uploads | █ | | | | | | | | | | | | | | create directories | █ | | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | | image viewer | █ | | █ | █ | █ | | | | █ | █ | █ | | █ | -| markdown viewer | █ | | | | █ | | | | █ | ╱ | ╱ | | █ | +| markdown viewer | █ | | / | | █ | | | | █ | ╱ | ╱ | | █ | | markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ | | █ | -| readme.md in listing | █ | | | █ | | | | | | | | | | +| readme.md in listing | █ | | / | █ | | | | | | | | | | | rename files | █ | █ | █ | █ | █ | ╱ | █ | | █ | █ | █ | █ | █ | | batch rename | █ | | | | | | | | █ | | | | | -| cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ | +| cut / paste files | █ | █ | █ | █ | █ | | | | █ | | | | █ | | move files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | | █ | | delete files | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | -| copy files | | | | | █ | | | | █ | █ | █ | | █ | +| copy files | | | / | | █ | | | | █ | █ | █ | | █ | * `single-page app` = multitasking; possible to continue navigating while uploading * `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song @@ -383,8 +384,6 @@ symbol legend, * `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads * `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best) * `b`/hfs2 has a very basic directory tree view, not showing sibling folders -* `c`/hfs3 remarks: - * audio playback does not continue into next song * `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav * `j`/filebrowser remarks: * audio playback does not continue into next song @@ -471,10 +470,8 @@ symbol legend, * vfs with gui config, per-volume permissions * tested locally, v0.53.2 on archlinux * 🔵 uploads are resumable -* ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare * ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic) * ⚠️ uploads are not integrity-checked -* ⚠️ copies the file after upload; need twice filesize free disk space * ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster) * ⚠️ doesn't support crazy filenames * ✅ config GUI From 15c5b50a36820da56b0af7fd184f305b11cacba2 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 20 Aug 2025 23:17:01 +0000 Subject: [PATCH 072/154] versus.md: additional corrections --- docs/versus.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/versus.md b/docs/versus.md index 5a0abdd0..70b3c21d 100644 --- a/docs/versus.md +++ b/docs/versus.md @@ -68,7 +68,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho * [kodbox](https://github.com/kalcaddle/kodbox) ([review](#kodbox)) appears to be a fantastic alternative if you're not worried about running chinese software, with several advantages over copyparty * but anything you want to share must be moved into the kodbox filesystem * [seafile](https://github.com/haiwen/seafile) ([review](#seafile)) and [nextcloud](https://github.com/nextcloud/server) ([review](#nextcloud)) could be decent alternatives if you need something heavier than copyparty - * but their [license](https://snyk.io/learn/agpl-license/) is [problematic](https://opensource.google/documentation/reference/using/agpl-policy) + * but their [license (AGPL)](https://snyk.io/learn/agpl-license/) is [thorny](https://opensource.google/documentation/reference/using/agpl-policy) * and copyparty is way better at uploads in particular (resumable, accelerated) * and anything you want to share must be moved into the respective filesystems * [filebrowser](https://github.com/filebrowser/filebrowser) ([review](#filebrowser)) and [dufs](https://github.com/sigoden/dufs) ([review](#dufs)) are simpler copyparties but with a settings gui @@ -123,7 +123,7 @@ symbol legend, | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | intuitive UX | | ╱ | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ | | config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ | -| good documentation | | | | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ | +| good documentation | | | █ | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ | | runs on iOS | ╱ | | | | | ╱ | | | | | | | | | runs on Android | █ | | █ | | | █ | | | | | | | | | runs on WinXP | █ | █ | | | | █ | | | | | | | | @@ -395,9 +395,9 @@ symbol legend, | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | -| OS alert on upload | █ | | | | | | | | | ╱ | | ╱ | | -| discord | █ | | | | | | | | | ╱ | | ╱ | | -| ┗ announce uploads | █ | | | | | | | | | | | ╱ | | +| OS alert on upload | ╱ | | | | | | | | | ╱ | | ╱ | | +| discord | ╱ | | | | | | | | | ╱ | | ╱ | | +| ┗ announce uploads | ╱ | | | | | | | | | | | ╱ | | | ┗ custom embeds | | | | | | | | | | | | ╱ | | | sharex | █ | | | █ | | █ | ╱ | █ | | | | | | | flameshot | | | | | | █ | | | | | | | | From cd8771fa522d1cf645c3c7e0193f07b53d81559c Mon Sep 17 00:00:00 2001 From: Daniel <daniel.xfuture@googlemail.com> Date: Thu, 21 Aug 2025 01:30:26 +0200 Subject: [PATCH 073/154] updated authelia docker-compose.yml (#377) --- .../basic-docker-compose/docker-compose.yml | 3 +- .../docker/idp-authelia-traefik/README.md | 12 ++- .../authelia/configuration.yml | 34 ++++---- .../idp-authelia-traefik/docker-compose.yml | 79 +++++++++++-------- 4 files changed, 67 insertions(+), 61 deletions(-) diff --git a/docs/examples/docker/basic-docker-compose/docker-compose.yml b/docs/examples/docker/basic-docker-compose/docker-compose.yml index dd3c1f9a..d75d3117 100644 --- a/docs/examples/docker/basic-docker-compose/docker-compose.yml +++ b/docs/examples/docker/basic-docker-compose/docker-compose.yml @@ -1,5 +1,6 @@ -services: +--- +services: copyparty: image: copyparty/ac:latest container_name: copyparty diff --git a/docs/examples/docker/idp-authelia-traefik/README.md b/docs/examples/docker/idp-authelia-traefik/README.md index 7667cb00..b083daff 100644 --- a/docs/examples/docker/idp-authelia-traefik/README.md +++ b/docs/examples/docker/idp-authelia-traefik/README.md @@ -8,7 +8,7 @@ to try this out with minimal adjustments: * login to https://fs.example.com/ with username `authelia` password `authelia` to use this in a safe and secure manner: -* follow a guide on setting up authelia properly (TODO:link) and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml` +* follow a guide on setting up [authelia](https://www.authelia.com/integration/proxies/traefik/#docker-compose) properly and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml` this folder is based on: * https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite @@ -16,20 +16,18 @@ this folder is based on: incomplete list of modifications made: * support for running with podman as root on fedora (`:z` volumes, `label:disable`) * explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release -* disabled automatic letsencrypt certificate signing * reduced logging from debug to info -* added a warning that traefik is given access to the docker socket (as recommended by traefik docs) which means traefik is able to break out of the container and has full root access on the host machine +* implemented a docker socket-proxy to not bind the docker.socket directly to traefik +* using valkey instead of redis for caching # security there is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62): -* Add in a redis password to limit attacker lateral movement in the system -* Move redis to a private network shared with just authelia -* Pin to image hashes (or go all in on updates and add `watchtower`) +* Move valkey to a private network shared with just authelia +* Add `watchtower` to manage your image version updates * Drop bridge networking for just exposing traefik's public ports -* Configure docker for non-root access to docker socket and then move traefik to use [non-root perms](https://docs.docker.com/engine/security/rootless/) if you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :> diff --git a/docs/examples/docker/idp-authelia-traefik/authelia/configuration.yml b/docs/examples/docker/idp-authelia-traefik/authelia/configuration.yml index b6ca311a..b4352511 100644 --- a/docs/examples/docker/idp-authelia-traefik/authelia/configuration.yml +++ b/docs/examples/docker/idp-authelia-traefik/authelia/configuration.yml @@ -1,15 +1,14 @@ -# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/configuration.yml - # Authelia configuration -# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE -jwt_secret: a_very_important_secret +identity_validation: + reset_password: + jwt_secret: 'a_very_important_secret_so_please_change_this' server: address: 'tcp://:9091' log: - level: info # debug + level: info totp: issuer: authelia.com @@ -21,29 +20,26 @@ authentication_backend: access_control: default_policy: deny rules: - # Rules applied to everyone - - domain: traefik.example.com - policy: one_factor + - domain: auth.example.com + policy: bypass # Allow access to the login UI - domain: fs.example.com policy: one_factor session: - # This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE secret: unsecure_session_secret - cookies: - name: authelia_session - domain: example.com # Should match whatever your root protected domain is + domain: example.com # this should match whatever your root protected domain is default_redirection_url: https://fs.example.com authelia_url: https://authelia.example.com/ expiration: 3600 # 1 hour inactivity: 300 # 5 minutes redis: - host: redis + host: valkey port: 6379 - # This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE - # password: authelia + password: your_secure_password_here + regulation: max_retries: 3 @@ -58,9 +54,7 @@ storage: notifier: disable_startup_check: true smtp: - username: test - # This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE - password: password - host: mail.example.com - port: 25 - sender: admin@example.com + address: 'smtp://127.0.0.1:25' + username: 'test' + password: 'password' + sender: "Authelia <admin@example.com>" diff --git a/docs/examples/docker/idp-authelia-traefik/docker-compose.yml b/docs/examples/docker/idp-authelia-traefik/docker-compose.yml index 9ebd73ba..3a45dda4 100644 --- a/docs/examples/docker/idp-authelia-traefik/docker-compose.yml +++ b/docs/examples/docker/idp-authelia-traefik/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.3' +--- networks: net: @@ -6,7 +6,7 @@ networks: services: copyparty: - image: copyparty/ac + image: copyparty/ac:latest container_name: idp_copyparty user: "1000:1000" # should match the user/group of your fileshare volumes volumes: @@ -19,19 +19,19 @@ services: labels: - 'traefik.enable=true' - 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)' - - 'traefik.http.routers.copyparty.entrypoints=https' + - 'traefik.http.routers.copyparty.entrypoints=websecure' - 'traefik.http.routers.copyparty.tls=true' + - 'traefik.http.routers.copyparty.tls.certresolver=letsencrypt' # ← THIS IS CRUCIAL - 'traefik.http.routers.copyparty.middlewares=authelia@docker' stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal environment: LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) - PYTHONUNBUFFERED: 1 # ensures log-messages are not delayed (but can reduce speed a tiny bit) authelia: - image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax + image: authelia/authelia:4.39.5@sha256:023e02e5203dfa0ebaee7a48b5bae34f393d1f9cada4a9df7fbf87eb1759c671 container_name: idp_authelia volumes: - ./authelia:/config:z @@ -40,25 +40,23 @@ services: labels: - 'traefik.enable=true' - 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)' - - 'traefik.http.routers.authelia.entrypoints=https' + - 'traefik.http.routers.authelia.entrypoints=websecure' - 'traefik.http.routers.authelia.tls=true' - #- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (1/2) + - 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' - 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com' - 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true' - 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email' expose: - 9091 restart: unless-stopped - healthcheck: - disable: true environment: - TZ=Etc/UTC - redis: - image: redis:7.2.4-alpine3.19 - container_name: idp_redis + valkey: + image: valkey/valkey:8.1.3-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4 + container_name: idp_valkey volumes: - - ./redis:/data:z + - ./valkey:/data:z networks: - net expose: @@ -66,40 +64,55 @@ services: restart: unless-stopped environment: - TZ=Etc/UTC + - VALKEY_EXTRA_FLAGS=--requirepass your_secure_password_here + + socket-proxy: + image: lscr.io/linuxserver/socket-proxy:3.2.3@sha256:63d2e0ce6bb0d12dfdbde5c3af31d08fee343ec3801a050c8197a3f5ffae8bed + container_name: idp_socket_proxy + environment: + - CONTAINERS=1 + - NETWORKS=1 + - EVENTS=1 + - PING=1 + - VERSION=1 + - LOG_LEVEL=warning + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /run + networks: + - net + restart: unless-stopped + expose: + - 2375 traefik: - image: traefik:2.11.0 + image: traefik:3.5.0@sha256:4e7175cfe19be83c6b928cae49dde2f2788fb307189a4dc9550b67acf30c11a5 container_name: idp_traefik volumes: - ./traefik:/etc/traefik:z - - /var/run/docker.sock:/var/run/docker.sock # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik - security_opt: - - label:disable # disable selinux because it (rightly) blocks access to docker.sock networks: - net labels: - 'traefik.enable=true' - - 'traefik.http.routers.api.rule=Host(`traefik.example.com`)' - - 'traefik.http.routers.api.entrypoints=https' - - 'traefik.http.routers.api.service=api@internal' - - 'traefik.http.routers.api.tls=true' - #- 'traefik.http.routers.api.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (2/2) - 'traefik.http.routers.api.middlewares=authelia@docker' ports: - '80:80' - '443:443' command: - - '--api' - - '--providers.docker=true' + - '--global.sendAnonymousUsage=false' + - '--providers.docker.endpoint=tcp://socket-proxy:2375' - '--providers.docker.exposedByDefault=false' - - '--entrypoints.http=true' - - '--entrypoints.http.address=:80' - - '--entrypoints.http.http.redirections.entrypoint.to=https' - - '--entrypoints.http.http.redirections.entrypoint.scheme=https' - - '--entrypoints.https=true' - - '--entrypoints.https.address=:443' + - '--entrypoints.web.address=:80' + - '--entrypoints.web.http.redirections.entrypoint.to=websecure' + - '--entrypoints.web.http.redirections.entrypoint.scheme=https' + - '--entrypoints.websecure.address=:443' - '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com' - '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json' - - '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http' - - '--log=true' - - '--log.level=WARNING' # DEBUG + - '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web' + - '--log.level=INFO' + depends_on: + - socket-proxy From 63d4ec64cd39650809c0286cd7507633dadbfc2d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 19:14:45 +0200 Subject: [PATCH 074/154] qr: fix fg=-1 with z=1 --- copyparty/tcpsrv.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index 55ea3a56..ff019caa 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -636,6 +636,8 @@ class TcpSrv(object): halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m" if not fg: halfc = "\033[0;40m{1}\033[0;47m" + if nocolor: + halfc = "\033[0;7m{1}\033[0m" def ansify(m: re.Match) -> str: return halfc.format(fg, " " * len(m.group(1)), bg) From 6c76614eb1e805b7837e4e9294b842b9db6c3d1f Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 19:35:36 +0200 Subject: [PATCH 075/154] fix rproxy hint; closes #661 --- copyparty/httpcli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index de3e6ede..885a5fb9 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -394,10 +394,10 @@ class HttpCli(object): zsl = [ " rproxy: %d if this client's IP-address is [%s]" % (-1 - zd, zs.strip()) - for zd, zs in enumerate(zsl) + for zd, zs in enumerate(zsl[::-1]) ] - t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:' - t = t % (self.args.xff_hdr,) + t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! The raw header value was [%s]. Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:' + t = t % (self.args.xff_hdr, zso) self.log("%s\n\n%s\n" % (t, "\n".join(zsl)), 3) pip = self.conn.addr[0] From ca98d54fda1774a12c0cb66fc48c29147bae0535 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 19:23:44 +0000 Subject: [PATCH 076/154] install on iOS; closes #328 --- README.md | 27 +++++++++++++++- contrib/setup-ashell.sh | 71 +++++++++++++++++++++++++++++++++++++++++ copyparty/__main__.py | 1 + copyparty/svchub.py | 9 +++++- docs/versus.md | 2 +- scripts/toc.sh | 2 +- tests/util.py | 2 +- 7 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 contrib/setup-ashell.sh diff --git a/README.md b/README.md index 7c42b833..a84f4b3b 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ made in Norway 🇳🇴 * [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+) * [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) * [install on android](#install-on-android) +* [install on iOS](#install-on-iOS) * [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them * [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md) @@ -155,6 +156,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/ * or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead * or install [on arch](#arch-package) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package) * or if you are on android, [install copyparty in termux](#install-on-android) +* or maybe an iPhone or iPad? [install in a-Shell on iOS](#install-on-iOS) * or maybe you have a [synology nas / dsm](./docs/synology-dsm.md) * or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty` * or if your computer is messed up and nothing else works, [try the pyz](#zipapp) @@ -241,7 +243,7 @@ also see [comparison to similar software](./docs/versus.md) * ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf) * ☑ [event hooks](#event-hooks) / script runner * ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy) - * ☑ cross-platform (Windows, Linux, Macos, Android, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64) + * ☑ cross-platform (Windows, Linux, Macos, Android, iOS, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64) * upload * ☑ basic: plain multipart, ie6 support * ☑ [up2k](#uploading): js, resumable, multithreaded @@ -2632,6 +2634,8 @@ there is no iPhone app, but the following shortcuts are almost as good: * can download links and rehost the target file on copyparty (see first comment inside the shortcut) * pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there +if you want to run the copyparty server on your iPhone or iPad, see [install on iOS](#install-on-iOS) + # performance @@ -2965,6 +2969,27 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132 * or if you want to use `vips` for photo-thumbs instead, `pkg install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})` +# install on iOS + +first install one of the following: +* [a-Shell mini](https://apps.apple.com/us/app/a-shell-mini/id1543537943) gives you the essential features +* [a-Shell](https://apps.apple.com/us/app/a-shell/id1473805438) also enables audio transcoding and better thubmnails + +and then copypaste the following command into `a-Shell`: + +```sh +curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh +``` + +what this does: +* creates a basic [config file](#accounts-and-volumes) named `cpc` which you can edit with `vim cpc` +* adds the command `cpp` to launch copyparty with that config file + +known issues: +* cannot run in the background; it needs to be on-screen to accept connections / uploads / downloads +* the best way to exit copyparty is to swipe away the app + + # reporting bugs ideas for context to include, and where to submit them diff --git a/contrib/setup-ashell.sh b/contrib/setup-ashell.sh new file mode 100644 index 00000000..006da444 --- /dev/null +++ b/contrib/setup-ashell.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# +# this script will install copyparty onto an iOS device (iPhone/iPad) +# +# step 1: install a-Shell: +# https://apps.apple.com/us/app/a-shell/id1473805438 +# +# step 2: copypaste the following command into a-Shell: +# curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh +# +# step 3: launch copyparty with this command: cpp +# +# if you ever want to upgrade copyparty, just repeat step 2 + + + +cd "$HOME/Documents" +curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py + + + +# create the config file? (cannot use heredoc because body too large) +[ -e cpc ] || { +echo '[global]' >cpc +echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc +echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc +echo ' e2ts # and enable multimedia indexing' >>cpc +echo ' ver # show copyparty version in the controlpanel' >>cpc +echo ' qrz: 2 # enable qr-code and make it big' >>cpc +echo ' qrp: 1 # reduce qr-code padding' >>cpc +echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc +echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc +echo '' >>cpc +echo ' # enable these by uncommenting them:' >>cpc +echo ' # ftp: 21 # enable ftp server on port 21' >>cpc +echo ' # tftp: 69 # enable tftp server on port 69' >>cpc +echo '' >>cpc +echo '[/]' >>cpc +echo ' ~/Documents' >>cpc +echo ' accs:' >>cpc +echo ' A: *' >>cpc +} + + + +# create the launcher? +[ -e cpp ] || { +echo '#!/bin/sh' >cpp +echo '' >>cpp +echo '# change the font so the qr-code draws correctly:' >>cpp +echo 'config -n "Menlo" # name' >>cpp +echo 'config -s 8 # size' >>cpp +echo '' >>cpp +echo '# launch copyparty' >>cpp +echo 'exec copyparty -c cpc "$@"' >>cpp +} + + + +chmod 755 copyparty cpp +echo +echo ================================= +echo +echo 'okay, all done!' +echo +echo 'you can edit your config' +echo 'with this command: vim cpc' +echo +echo 'you can run copyparty' +echo 'with this command: cpp' +echo diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 1a0ed743..e14f0573 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1114,6 +1114,7 @@ def add_qr(ap, tty): ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)") ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr") + ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log") def add_fs(ap): diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 92a797d4..39ac5093 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -819,6 +819,10 @@ class SvcHub(object): t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url) self.pr(t, file=sys.stderr) + def sleepy_qr(self): + time.sleep(self.args.qr_wait) + self.log("qr-code", self.tcpsrv.qr) + def cb_httpsrv_up(self) -> None: self.httpsrv_up += 1 if self.httpsrv_up != self.broker.num_workers: @@ -834,7 +838,10 @@ class SvcHub(object): if self.args.qr_pin: self.sticky_qr() else: - self.log("qr-code", self.tcpsrv.qr) + if self.args.qr_wait: + Daemon(self.sleepy_qr, "qr_w8") + else: + self.log("qr-code", self.tcpsrv.qr) else: self.log("root", "workers OK\n") diff --git a/docs/versus.md b/docs/versus.md index 70b3c21d..d505a51a 100644 --- a/docs/versus.md +++ b/docs/versus.md @@ -140,7 +140,7 @@ symbol legend, * `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever * `a`/copyparty remarks: * no gui for server settings; only for client-side stuff - * can theoretically run on iOS / iPads using [iSH](https://ish.app/), but only the iPad will offer sufficient multitasking i think + * runs on iOS / iPads using [a-Shell](https://holzschu.github.io/a-Shell_iOS/) (pretty good) or [iSH](https://ish.app/) (very slow) but cannot run in the background and is not able to share all of your phone storage (just a separate dedicated folder) * [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only * no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading * `b`/hfs2 runs on linux through wine diff --git a/scripts/toc.sh b/scripts/toc.sh index 8c3e22e9..611e092a 100755 --- a/scripts/toc.sh +++ b/scripts/toc.sh @@ -20,7 +20,7 @@ cat $f | awk ' o{next} /^#/{s=1;rs=0;pr()} /^#* *(nix package)/{rs=1} - /^#* *(themes|install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module|reverse-proxy perf)|```/{s=rs} + /^#* *(themes|install on android|install on iOS|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module|reverse-proxy perf)|```/{s=rs} /^#/{ lv=length($1); sub(/[^ ]+ /,""); diff --git a/tests/util.py b/tests/util.py index 546851b4..897bc108 100644 --- a/tests/util.py +++ b/tests/util.py @@ -161,7 +161,7 @@ class Cfg(Namespace): ex = "ac_convt au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" ka.update(**{k: 9 for k in ex.split()}) - ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" + 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 chmod_f chpw_db doctitle df exit favico ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" From 0de07d8e8b25c8c21a25c183a2f523537a2b6d45 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 19:39:48 +0000 Subject: [PATCH 077/154] fix a11y crash; closes #649 --- copyparty/web/browser.css | 1 + copyparty/web/browser.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 8b87d4c2..90cb9be8 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -571,6 +571,7 @@ pre, code, tt, #doc, #doc>code { overflow: hidden; width: 0; height: 0; + left: -10em; color: var(--bg); } html .ayjump:focus { diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 002be4d9..1e8a9828 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -13420,9 +13420,11 @@ function eval_hash() { d.onclick = function (e) { ev(e); if (a) - QS(treectl.hidden ? '#path a:nth-last-child(2)' : '#treeul a.hl').focus(); + d = QS(treectl.hidden ? '#path a:nth-last-child(2)' : '#treeul a.hl'); else - QS(thegrid.en ? '#ggrid a' : '#files tbody tr[tabindex]').focus(); + d = QS(thegrid.en ? '#ggrid a' : '#files tbody tr[tabindex]'); + if (d) + d.focus(); }; })(a); From 6d76254c88af641b209537b6044dc6aa8d1251b0 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 20:28:21 +0000 Subject: [PATCH 078/154] ftpd: fix ipv6 bonks (#628) --- copyparty/ftpd.py | 8 ++++---- copyparty/svchub.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index b7246291..197ca59d 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -68,13 +68,13 @@ class FtpAuth(DummyAuthorizer): if ip.startswith("::ffff:"): ip = ip[7:] - ip = ipnorm(ip) + ipn = ipnorm(ip) bans = self.hub.bans - if ip in bans: - rt = bans[ip] - time.time() + if ipn in bans: + rt = bans[ipn] - time.time() if rt < 0: logging.info("client unbanned") - del bans[ip] + del bans[ipn] else: raise AuthenticationFailed("banned") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 39ac5093..199ccce9 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -433,7 +433,7 @@ class SvcHub(object): # create netmaps early to avoid firewall gaps, # but the mutex blocks multiprocessing startup - for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split(): + for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split(): try: getattr(args, zs).mutex = threading.Lock() except: From cc65b1b551a3ebf7320d84bc1f996f4e7ce9ffdd Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 21:26:13 +0000 Subject: [PATCH 079/154] more helpful configparser --- copyparty/authsrv.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 0c8a5ab4..de8e764c 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1328,6 +1328,10 @@ class AuthSrv(object): zt = split_cfg_ln(ln) for zs, za in zt.items(): zs = zs.lstrip("-") + if "=" in zs: + t = "WARNING: found an option named [%s] in your [global] config; did you mean to say [%s: %s] instead?" + zs1, zs2 = zs.split("=", 1) + self.log(t % (zs, zs1, zs2), 3) if za is True: self._e("└─argument [{}]".format(zs)) else: @@ -1337,6 +1341,10 @@ class AuthSrv(object): if cat == cata: try: u, p = [zs.strip() for zs in ln.split(":", 1)] + if "=" in u and not p: + t = "WARNING: found username [%s] in your [accounts] config; did you mean to say [%s: %s] instead?" + zs1, zs2 = u.split("=", 1) + self.log(t % (u, zs1, zs2), 3) self._l(ln, 5, "account [{}], password [{}]".format(u, p)) acct[u] = p except: @@ -1407,6 +1415,10 @@ class AuthSrv(object): zd = split_cfg_ln(ln) fstr = "" for sk, sv in zd.items(): + if "=" in sk: + t = "WARNING: found a volflag named [%s] in your config; did you mean to say [%s: %s] instead?" + zs1, zs2 = sk.split("=", 1) + self.log(t % (sk, zs1, zs2), 3) bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-") if bad: err = "bad characters [{}] in volflag name [{}]; " From f9cb2c15e386607430379ad8d87af64e440663e0 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 21:50:56 +0000 Subject: [PATCH 080/154] readme: homebrew --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index a84f4b3b..e0ad0162 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ made in Norway 🇳🇴 * [packages](#packages) - the party might be closer than you think * [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/)) * [fedora package](#fedora-package) - does not exist yet + * [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg` * [nix package](#nix-package) - `nix profile install github:9001/copyparty` * [nixos module](#nixos-module) * [browser support](#browser-support) - TLDR: yes @@ -1320,6 +1321,14 @@ some recommended FTP / FTPS clients; `wark` = example password: * `lftp -u k,wark -p 3921 127.0.0.1 -e ls` * `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'` +config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall: + +```yaml +[global] + ftp: 3921 + ftp-pr: 12000-12099 +``` + ## webdav server @@ -2333,6 +2342,15 @@ after installing, start either the system service or the user service and naviga does not exist yet; there are rumours that it is being packaged! keep an eye on this space... +## homebrew formulae + +`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty + +should work on all macs (both intel and apple silicon) and all relevant macos versions + +the homebrew package is maintained by the homebrew team (thanks!) + + ## nix package `nix profile install github:9001/copyparty` From cc4f4aef99b07593312ed8e978c05b5ad02d5b87 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 22:03:34 +0000 Subject: [PATCH 081/154] fix typos; closes #237, closes #465 correct subset of fixes, not touching the intentional/sacred ones --- README.md | 8 ++++---- bin/dbtool.py | 4 ++-- bin/handlers/redirect.py | 4 ++-- bin/hooks/notify.py | 2 +- bin/hooks/wget.py | 2 +- bin/mtag/guestbook-read.py | 2 +- bin/mtag/guestbook.py | 2 +- bin/mtag/wget.py | 2 +- contrib/nginx/copyparty.conf | 2 +- copyparty/__main__.py | 2 +- copyparty/web/md.js | 2 +- copyparty/web/up2k.js | 2 +- docs/devnotes.md | 2 +- docs/examples/docker/idp-authelia-traefik/README.md | 2 +- docs/notes.sh | 4 +++- docs/versus.md | 2 +- scripts/make-sfx.sh | 2 +- 17 files changed, 24 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e0ad0162..00f18cfc 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ also see [comparison to similar software](./docs/versus.md) * ☑ play video files as audio (converted on server) * ☑ create and play [m3u8 playlists](#playlists) * ☑ image gallery with webm player - * ☑ [textfile browser](#textfile-viewer) with syntax hilighting + * ☑ [textfile browser](#textfile-viewer) with syntax highlighting * ☑ realtime streaming of growing files (logfiles and such) * ☑ [thumbnails](#thumbnails) * ☑ ...of images using Pillow, pyvips, or FFmpeg @@ -834,7 +834,7 @@ the up2k UI is the epitome of polished intuitive experiences: * `[🔎]` switch between upload and [file-search](#file-search) mode * ignore `[🔎]` if you add files by dragging them into the browser -and then theres the tabs below it, +and then there's the tabs below it, * `[ok]` is the files which completed successfully * `[ng]` is the ones that failed / got rejected (already exists, ...) * `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order @@ -1066,7 +1066,7 @@ plays almost every audio format there is (if the server has FFmpeg installed fo the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav` -some hilights: +some highlights: * OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png)) * shows the audio waveform in the seekbar * not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended @@ -2171,7 +2171,7 @@ when connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-f in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet) -* if these results are bullshit because my config exampels are bad, please submit corrections! +* if these results are bullshit because my config examples are bad, please submit corrections! ## permanent cloudflare tunnel diff --git a/bin/dbtool.py b/bin/dbtool.py index ff92fca3..12e3d535 100755 --- a/bin/dbtool.py +++ b/bin/dbtool.py @@ -8,7 +8,7 @@ import sqlite3 import argparse DB_VER1 = 3 -DB_VER2 = 5 +DB_VER2 = 6 BY_PATH = None NC = None @@ -39,7 +39,7 @@ def ls(db): print(f"{nfiles} files") print(f"{ntags} tags\n") - print("number of occurences for each tag,") + print("number of occurrences for each tag,") print(" 'x' = file has no tags") print(" 't:mtp' = the mtp flag (file not mtp processed yet)") print() diff --git a/bin/handlers/redirect.py b/bin/handlers/redirect.py index 07b91d0c..fde29454 100644 --- a/bin/handlers/redirect.py +++ b/bin/handlers/redirect.py @@ -46,7 +46,7 @@ def main(cli, vn, rem): # uncomment one of these: send_http_302_temporary_redirect(cli, new_path) - #send_http_301_permanent_redirect(cli, new_path) - #send_errorpage_with_redirect_link(cli, new_path) + # send_http_301_permanent_redirect(cli, new_path) + # send_errorpage_with_redirect_link(cli, new_path) return "true" diff --git a/bin/hooks/notify.py b/bin/hooks/notify.py index 6e5dda86..bc12269d 100755 --- a/bin/hooks/notify.py +++ b/bin/hooks/notify.py @@ -9,7 +9,7 @@ from plyer import notification _ = r""" show os notification on upload; works on windows, linux, macos, android -depdencies: +dependencies: windows: python3 -m pip install --user -U plyer linux: python3 -m pip install --user -U plyer macos: python3 -m pip install --user -U plyer pyobjus diff --git a/bin/hooks/wget.py b/bin/hooks/wget.py index ad0c71c0..1f5a824b 100755 --- a/bin/hooks/wget.py +++ b/bin/hooks/wget.py @@ -66,7 +66,7 @@ def main(): try: sp.check_call(cmd) except: - t = "-- FAILED TO DONWLOAD " + name + t = "-- FAILED TO DOWNLOAD " + name print(f"{t}\n", end="") open(t, "wb").close() diff --git a/bin/mtag/guestbook-read.py b/bin/mtag/guestbook-read.py index 704addbe..e9119903 100755 --- a/bin/mtag/guestbook-read.py +++ b/bin/mtag/guestbook-read.py @@ -7,7 +7,7 @@ example copyparty config to use this: --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook explained: - for realpath srv/hello (served at /hello), write-only for eveyrone, + for realpath srv/hello (served at /hello), write-only for everyone, enable file analysis on upload (e2ts), use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook", do this on all uploads regardless of extension, diff --git a/bin/mtag/guestbook.py b/bin/mtag/guestbook.py index 437289b6..84f8fa53 100644 --- a/bin/mtag/guestbook.py +++ b/bin/mtag/guestbook.py @@ -11,7 +11,7 @@ example copyparty config to use this: --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb explained: - for realpath srv/hello (served at /hello),write-only for eveyrone, + for realpath srv/hello (served at /hello),write-only for everyone, enable file analysis on upload (e2ts), use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb", do this on all uploads with the file extension "bin", diff --git a/bin/mtag/wget.py b/bin/mtag/wget.py index 26a1fa45..d706052b 100644 --- a/bin/mtag/wget.py +++ b/bin/mtag/wget.py @@ -84,7 +84,7 @@ def main(): # on success, delete the .bin file which contains the URL os.unlink(fp) except: - open("-- FAILED TO DONWLOAD " + name, "wb").close() + open("-- FAILED TO DOWNLOAD " + name, "wb").close() os.unlink(tfn) print(url) diff --git a/contrib/nginx/copyparty.conf b/contrib/nginx/copyparty.conf index 121e52ab..f0d382cc 100644 --- a/contrib/nginx/copyparty.conf +++ b/contrib/nginx/copyparty.conf @@ -31,7 +31,7 @@ # generate the list of permitted IP ranges like so: # (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf # -# and then enable it below by uncomenting the cloudflare-only.conf line +# and then enable it below by uncommenting the cloudflare-only.conf line # # ====================================================================== diff --git a/copyparty/__main__.py b/copyparty/__main__.py index e14f0573..8467fd81 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -775,7 +775,7 @@ def get_sects(): \033[36mc0\033[35m show all process output (default) \033[36mc1\033[35m show only stderr \033[36mc2\033[35m show only stdout - \033[36mc3\033[35m mute all process otput + \033[36mc3\033[35m mute all process output \033[0m examples: diff --git a/copyparty/web/md.js b/copyparty/web/md.js index 0109d453..9ab5a2fc 100644 --- a/copyparty/web/md.js +++ b/copyparty/web/md.js @@ -422,7 +422,7 @@ function init_toc() { } } - // hilight the correct toc items + scroll into view + // highlight the correct toc items + scroll into view function freshen_toclist() { if (anchors.length == 0) return; diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index 6ae7046f..a7094859 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -50,7 +50,7 @@ catch (ex) { } catch (ex) { console.log('up2k init failed:', ex); - toast.err(10, 'could not initialze up2k\n\n' + basenames(ex)); + toast.err(10, 'could not initialize up2k\n\n' + basenames(ex)); } } treectl.onscroll(); diff --git a/docs/devnotes.md b/docs/devnotes.md index e2fcc008..04c37764 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -328,7 +328,7 @@ if you don't need all the features, you can repack the sfx and save a bunch of s the features you can opt to drop are * `cm`/easymde, the "fancy" markdown editor, saves ~89k -* `hl`, prism, the syntax hilighter, saves ~41k +* `hl`, prism, the syntax highlighter, saves ~41k * `fnt`, source-code-pro, the monospace font, saves ~9k for the `re`pack to work, first run one of the sfx'es once to unpack it diff --git a/docs/examples/docker/idp-authelia-traefik/README.md b/docs/examples/docker/idp-authelia-traefik/README.md index b083daff..bc7a7f2a 100644 --- a/docs/examples/docker/idp-authelia-traefik/README.md +++ b/docs/examples/docker/idp-authelia-traefik/README.md @@ -45,4 +45,4 @@ currently **not optimal,** at least when compared to running the python sfx outs authelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there... -I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't relly know where to start +I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't really know where to start diff --git a/docs/notes.sh b/docs/notes.sh index 3c8798cc..9152d4ac 100644 --- a/docs/notes.sh +++ b/docs/notes.sh @@ -71,7 +71,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}' -# or if the client youre measuring dies for ~15sec every once ina while and you wanna filter those out, +# or if the client you're measuring dies for ~15sec every once ina while and you wanna filter those out, cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf "%.3f += %.3f - %.3f (%.3f) # %.3f -> %.3f\n",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}' @@ -337,3 +337,5 @@ mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfi mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0" mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0" +# number of megabytes downloaded since some date +awk </var/log/wjaycore.out '/^..36m2025-05-20/{o=1} !o{next} !/ plain 20[06](,| \[[^,]+\],) +[0-9.]+.\[33m[KM] .* n[0-9]+$/{next} {v=$0;sub(/.* plain 20[06](,| \[[^,]+\],) +/,"",v);sub(/ .*/,"",v);u=v;sub(/.\[.*/,"",v);sub(/.*m/,"",u);$0=u} /[KMG]/{v*=1024} /[MG]/{v*=1024} /G/{v*=1024} {t+=v} END{printf "%d\n",t/(1024*1024)}' diff --git a/docs/versus.md b/docs/versus.md index d505a51a..685aa14b 100644 --- a/docs/versus.md +++ b/docs/versus.md @@ -572,7 +572,7 @@ symbol legend, * ✅ file tags; file discussions!? * ✅ video transcoding * ✅ unzip uploaded archives -* ✅ IDE with syntax hilighting +* ✅ IDE with syntax highlighting * ✅ wysiwyg editor for openoffice files ## [filebrowser](https://github.com/filebrowser/filebrowser) diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index c3552a63..37ce8d0f 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -41,7 +41,7 @@ help() { exec cat <<'EOF' # `no-cm` saves ~89k by removing easymde/codemirror # (the fancy markdown editor) # -# `no-hl` saves ~41k by removing syntax hilighting in the text viewer +# `no-hl` saves ~41k by removing syntax highlighting in the text viewer # # `no-fnt` saves ~9k by removing the source-code-pro font # (browsers will try to use 'Consolas' instead) From 202ddeac0df83b53a2a3d84e49889259325ace2d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 23:06:39 +0000 Subject: [PATCH 082/154] write qrcode to file --- copyparty/__main__.py | 1 + copyparty/stolen/qrcodegen.py | 19 +++++++++++++++++++ copyparty/tcpsrv.py | 32 +++++++++++++++++++++++++++++++- tests/util.py | 2 +- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 8467fd81..3cb97949 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1115,6 +1115,7 @@ def add_qr(ap, tty): ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr") ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log") + ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent") def add_fs(ap): diff --git a/copyparty/stolen/qrcodegen.py b/copyparty/stolen/qrcodegen.py index 38296359..8790e859 100644 --- a/copyparty/stolen/qrcodegen.py +++ b/copyparty/stolen/qrcodegen.py @@ -200,6 +200,25 @@ class QrCode(object): return "\n".join(rows) + def to_png(self, zoom, pad, bg, fg, ap) -> None: + from PIL import Image + + tab = self.modules + sz = self.size + psz = sz + pad * 2 + if bg: + img = Image.new("RGB", (psz, psz), bg) + else: + img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0)) + fg = (fg[0], fg[1], fg[2], 255) + for y in range(sz): + for x in range(sz): + if tab[y][x]: + img.putpixel((x + pad, y + pad), fg) + if zoom != 1: + img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST) + img.save(ap) + def _draw_function_patterns(self) -> None: # Draw horizontal and vertical timing patterns for i in range(self.size): diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index ff019caa..8d355251 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -9,7 +9,7 @@ import time from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .cert import gencert -from .stolen.qrcodegen import QrCode +from .stolen.qrcodegen import QrCode, qr2svg from .util import ( E_ACCESS, E_ADDR_IN_USE, @@ -621,6 +621,10 @@ class TcpSrv(object): pad = self.args.qrp zoom = self.args.qrz qrc = QrCode.encode_binary(btxt) + + for zs in self.args.qr_file or []: + self._qr2file(qrc, zs) + if zoom == 0: try: tw, th = termsize() @@ -656,3 +660,29 @@ class TcpSrv(object): t = t.replace("\n", "`\n`") return txt + t + + def _qr2file(self, qrc: QrCode, txt: str): + if ".txt:" in txt or ".svg:" in txt: + ap, zs1, zs2 = txt.rsplit(":", 2) + bg = fg = "" + else: + ap, zs1, zs2, bg, fg = txt.rsplit(":", 4) + zoom = int(zs1) + pad = int(zs2) + + if ap.endswith(".txt"): + if zoom not in (1, 2): + raise Exception("invalid zoom for qr.txt; must be 1 or 2") + with open(ap, "wb") as f: + f.write(qrc.render(zoom, pad).encode("utf-8")) + elif ap.endswith(".svg"): + with open(ap, "wb") as f: + f.write(qr2svg(qrc, pad).encode("utf-8")) + else: + qrc.to_png(zoom, pad, self._h2i(bg), self._h2i(fg), ap) + + def _h2i(self, hs): + try: + return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4)) + except: + return None diff --git a/tests/util.py b/tests/util.py index 897bc108..2318bab7 100644 --- a/tests/util.py +++ b/tests/util.py @@ -170,7 +170,7 @@ class Cfg(Namespace): ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner" ka.update(**{k: "no" for k in ex.split()}) - ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" + 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" ka.update(**{k: [] for k in ex.split()}) ex = "exp_lg exp_md" From ceaf133d9d89f287f73d19bc14b1b16cdb72d7eb Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 23:19:11 +0000 Subject: [PATCH 083/154] v1.19.5 --- README.md | 6 +++++ copyparty/__version__.py | 4 ++-- docs/changelog.md | 51 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 00f18cfc..530d02d0 100644 --- a/README.md +++ b/README.md @@ -1297,6 +1297,12 @@ print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/ * `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2` * `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size * 1x may render incorrectly on some terminals/fonts, but 2x should always work +* `--qr-pin 1` makes the qr-code stick to the bottom of the console (never scrolls away) +* `--qr-file qr.txt:1:2` writes a small qr-code to `qr.txt` +* `--qr-file qr.txt:2:2` writes a big qr-code to `qr.txt` +* `--qr-file qr.svg:1:2` writes a vector-graphics qr-code to `qr.svg` +* `--qr-file qr.png:8:4:333333:ffcc55` writes an 8x-magnified yellow-on-gray `qr.png` +* `--qr-file qr.png:8:4::ffffff` writes an 8x-magnified white-on-transparent `qr.png` it uses the server hostname if [mdns](#mdns) is enabled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 639b85ba..4703e216 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 4) +VERSION = (1, 19, 5) CODENAME = "usernames" -BUILD_DT = (2025, 8, 17) +BUILD_DT = (2025, 8, 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 a1dfa881..8c4fbaf5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,54 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-0817-1556 `v1.19.4` take two (fix cfg vols) + +## ℹ️ this upgrade is a one-way ticket + +* your up2k database (`.hist/up2k.db`), used by the `e2d` filesystem indexing feature, **will be upgraded to a new format** which older copyparty versions cannot read. A backup of each database will be created automatically, named `up2k.db.bak.SOMETHING.v5`. If you need to downgrade to a previous version: Shutdown copyparty, delete these files: `up2k.db up2k.db-shm up2k.db-wal` and then copy `up2k.db.bak.*.v5` to `up2k.db` + +## 🧪 new features + +* new translations: + * #551 Swedish (thx @Bevinsky!) d676a86f + * #551 Korean (thx @nyqui!) 4e878d2f +* #581 new theme: phi95 (thx @varphi-online!) d8662aeb +* #567 .raw image thumbnails (thx @ar-nelson!) 0177a9b4 + * available in docker-images `iv` and `dj` +* #561 epub thumbnails (thx @Scotsguy!) 9435e6b2 +* #252 music thumbnails use embdded coverart if available 98d117b8 + * thumbnails folder `.hist/th` must be deleted to take effect +* #530 show username of uploaders in file listings; requires `a` (admin) permission 4df033ec +* #604 a new group `@acct` which automatically contains all known usernames 68907eaf +* controlpanel has a dedicated "logout all sessions" button, similar to the logout-link in the browser f4a3fba2 +* #397 accounts can be restricted to certian IPs 62e072a2 +* #504 automatic login through tailscale auth a4649d1e +* #533 sticky qr-code with `--qr-pin 1` 1ebe06f5 +* #572 button to abort copy/move 715d374e +* #618 "download selected files" didn't work on firefox 52 (winxp) dcc6b1b4 +* max number of cookies to allow can be configured 6303effe + * good if you have too many selfhosted services on one domain (but will beware of the spec-mandataed max length of the cookie field!) + +## 🩹 bugfixes + +* fix xvol/xdev edgecases: + * #603 rootless vfs 554cc2f3 + * false-positive with overlapping volumes d9046f7e +* #573 ftp: attempting an upload into read-only folder no longer kills the connection 3aa8b7aa +* #306 adjust navpane for `--rp-loc` (location-based proxying) +* #556 more sensible config expansion order f4727f8e + * #624 ...which broke things bf1fdcab +* the video player now stays fullscreen between videos 782e2f1d +* heif thumbnailing with libvips + +## 🔧 other changes + +* #253 build nix-packages from source (thx @toast003, @chinponya!) 187cae25 +* #616 logfiles will have a plaintext severity column if `--no-ansi` d4cf42e7 +* #598 separate option `--ac-convt` for audio transcoding timeout d5623057 +* #596 users with a blank password gets a strong random-generated one 7f448750 +* copyparty.exe: upgrade to python 3.13.7 + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-0810-1226 `v1.19.1` archlinux fix From 5c250c2c199c28f1eec4f9e814c777ae1b6155a3 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 21 Aug 2025 23:41:08 +0000 Subject: [PATCH 084/154] update pkgs to 1.19.5 --- 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 355d6cbc..d115019b 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.19.4" +pkgver="1.19.5" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb") +sha256sums=("366e0e73d0e322e1dba9315baa328a6cd488f272cb431b9187da326361c2d7da") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index c3fb4186..1a64f4c4 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.4 +pkgver=1.19.5 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb") +sha256sums=("366e0e73d0e322e1dba9315baa328a6cd488f272cb431b9187da326361c2d7da") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 2aaef28d..80347478 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.19.4/copyparty-1.19.4.tar.gz", - "version": "1.19.4", - "hash": "sha256-sOhKeOsnAct0R7YCOvzsKAxVBhfd5ntvAoW7I0gxEes=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.5/copyparty-1.19.5.tar.gz", + "version": "1.19.5", + "hash": "sha256-Nm4Oc9DjIuHbqTFbqjKKbNSI8nLLQxuRh9oyY2HC19o=" } \ No newline at end of file From d39c74c126e39227a2138f360d43456269f2d456 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 22 Aug 2025 21:43:31 +0000 Subject: [PATCH 085/154] fix fe80 assumption; IPv6 link-local is fe80::/10, not just fe80 --- copyparty/mdns.py | 4 ++-- copyparty/multicast.py | 6 +++--- copyparty/tcpsrv.py | 7 ++++--- copyparty/util.py | 2 ++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/copyparty/mdns.py b/copyparty/mdns.py index 0dde707d..d80e5142 100644 --- a/copyparty/mdns.py +++ b/copyparty/mdns.py @@ -27,7 +27,7 @@ from .stolen.dnslib import ( DNSRecord, set_avahi_379, ) -from .util import CachedSet, Daemon, Netdev, list_ips, min_ex +from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex if TYPE_CHECKING: from .svchub import SvcHub @@ -375,7 +375,7 @@ class MDNS(MCast): cip = addr[0] v6 = ":" in cip if (cip.startswith("169.254") and not self.ll_ok) or ( - v6 and not cip.startswith("fe80") + v6 and not cip.startswith(IP6_LL) ): return diff --git a/copyparty/multicast.py b/copyparty/multicast.py index 766dc15d..ea2f223e 100644 --- a/copyparty/multicast.py +++ b/copyparty/multicast.py @@ -15,7 +15,7 @@ from ipaddress import ( ) from .__init__ import MACOS, TYPE_CHECKING -from .util import Daemon, Netdev, find_prefix, min_ex, spack +from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack if TYPE_CHECKING: from .svchub import SvcHub @@ -145,7 +145,7 @@ class MCast(object): all_selected = ips[:] # discard non-linklocal ipv6 - ips = [x for x in ips if ":" not in x or x.startswith("fe80")] + ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)] if not ips: raise NoIPs() @@ -183,7 +183,7 @@ class MCast(object): srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False) # gvfs breaks if a linklocal ip appears in a dns reply - ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))} + ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)} rt = {k: v for k, v in srv.ips.items() if k not in ll} if self.args.ll or not rt: diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index 8d355251..b29b75f7 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -16,6 +16,7 @@ from .util import ( E_ADDR_NOT_AVAIL, E_UNREACH, HAVE_IPV6, + IP6_LL, IP6ALL, VF_CAREFUL, Netdev, @@ -140,12 +141,12 @@ class TcpSrv(object): # keep IPv6 LL-only nics ll_ok: set[str] = set() for ip, nd in self.netdevs.items(): - if not ip.startswith("fe80"): + if not ip.startswith(IP6_LL): continue just_ll = True for ip2, nd2 in self.netdevs.items(): - if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"): + if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL): just_ll = False if just_ll or self.args.ll: @@ -164,7 +165,7 @@ class TcpSrv(object): title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")] t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)" for ip, desc in sorted(eps.items(), key=lambda x: x[1]): - if ip.startswith("fe80") and ip not in ll_ok: + if ip.startswith(IP6_LL) and ip not in ll_ok: continue for port in sorted(self.args.p): diff --git a/copyparty/util.py b/copyparty/util.py index 7e9d4bb1..97351f3c 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -112,6 +112,8 @@ E_ACCESS = _ens("EACCES WSAEACCES") E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH") IP6ALL = "0:0:0:0:0:0:0:0" +IP6_LL = ("fe8", "fe9", "fea", "feb") +IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254") try: From 978801d02007865d6a1b6feb7bf1ff5bf68ce546 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 22 Aug 2025 22:26:51 +0000 Subject: [PATCH 086/154] ftp: fix link-local IPv6; closes #628 --- scripts/make-sfx.sh | 1 + scripts/patches/pyftpdlib-fe80.patch | 37 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 scripts/patches/pyftpdlib-fe80.patch diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index 37ce8d0f..100792ca 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -218,6 +218,7 @@ necho() { mv pyftpdlib-*/pyftpdlib . rm -rf pyftpdlib-* pyftpdlib/test patch -s -p1 <../scripts/patches/pyftpdlib-win313.patch + patch -s -p1 <../scripts/patches/pyftpdlib-fe80.patch for f in pyftpdlib/_async{hat,ore}.py; do [ -e "$f" ] || continue; iawk 'NR<4||NR>27||!/^#/;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' $f diff --git a/scripts/patches/pyftpdlib-fe80.patch b/scripts/patches/pyftpdlib-fe80.patch new file mode 100644 index 00000000..48b0cae1 --- /dev/null +++ b/scripts/patches/pyftpdlib-fe80.patch @@ -0,0 +1,37 @@ +accept connections from IPv6 link-local addresses + +diff -NarU1 a/pyftpdlib/handlers.py b/pyftpdlib2/handlers.py +--- a/pyftpdlib/handlers.py 2024-06-23 14:03:38 ++++ b/pyftpdlib/handlers.py 2025-08-22 21:59:40 +@@ -451,3 +451,4 @@ + +- local_ip = self.cmd_channel.socket.getsockname()[0] ++ sockname = list(self.cmd_channel.socket.getsockname()) ++ local_ip = sockname[0] + if local_ip in self.cmd_channel.masquerade_address_map: +@@ -459,3 +460,5 @@ + +- if self.cmd_channel.server.socket.family != socket.AF_INET: ++ if local_ip.startswith('fe') and local_ip[2:3] in "89ab": ++ af = socket.AF_INET6 # link-local ++ elif self.cmd_channel.server.socket.family != socket.AF_INET: + # dual stack IPv4/IPv6 support +@@ -472,3 +475,4 @@ + # free unprivileged random port. +- self.bind((local_ip, 0)) ++ sockname[1] = 0 ++ self.bind(tuple(sockname)) + else: +@@ -478,4 +482,5 @@ + self.set_reuse_addr() ++ sockname[1] = port + try: +- self.bind((local_ip, port)) ++ self.bind(tuple(sockname)) + except PermissionError: +@@ -495,3 +500,4 @@ + else: +- self.bind((local_ip, 0)) ++ sockname[1] = 0 ++ self.bind(tuple(sockname)) + self.cmd_channel.log( From ad0e6c7fde1fc67a681ff1b69426b001c8b43b4b Mon Sep 17 00:00:00 2001 From: nyqui <67160376+nyqui@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:40:39 +0900 Subject: [PATCH 087/154] updated strings "mt_cflac", "mt_caac", "mt_coth" --- copyparty/web/browser.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 1e8a9828..5f3fbb1e 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -5975,9 +5975,9 @@ var Ls = { "mt_mloop": "열린 폴더 반복\">🔁 반복", "mt_mnext": "다음 폴더 불러오고 계속\">📂 다음", "mt_mstop": "재생 중지\">⏸ 중지", - "mt_cflac": "flac/wav를 opus로 변환\">flac", - "mt_caac": "aac/m4a를 opus로 변환\">aac", - "mt_coth": "다른 모든 것 (mp3 제외)을 opus로 변환\">기타", + "mt_cflac": "flac/wav를 {0}로 변환\">flac", + "mt_caac": "aac/m4a를 {0}로 변환\">aac", + "mt_coth": "다른 모든 것 (mp3 제외)을 {0}로 변환\">기타", "mt_c2opus": "데스크톱, 노트북, 안드로이드 환경에 최적\">opus", "mt_c2owa": "iOS 17.5 이상용 opus-weba\">owa", "mt_c2caf": "iOS 11부터 17까지용 opus-caf\">caf", From 6413ad3e8d568348f72818d1ad7fc465396ac008 Mon Sep 17 00:00:00 2001 From: Gus P <guspumat@gmail.com> Date: Sat, 23 Aug 2025 10:35:13 +0200 Subject: [PATCH 088/154] fix LLM typo in CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index edc53c69..a067404f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,13 +6,13 @@ but please: -# do not use AI / LMM when writing code +# do not use AI / LLM when writing code copyparty is 100% organic, free-range, human-written software! > ⚠ you are now entering a no-copilot zone -the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate. +the *only* place where LLM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate. sorry for the harsh tone, but this is important to me 🙏 From 6cd0a396df5fad77e04ff5717130bbd47bd3fdd2 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 23 Aug 2025 19:45:18 +0000 Subject: [PATCH 089/154] readme: clarify smb perf + u2c needs e2dsa (#678) --- README.md | 8 ++++++++ bin/README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 530d02d0..e13f6bf3 100644 --- a/README.md +++ b/README.md @@ -1429,6 +1429,7 @@ and some minor issues, * win10 onwards does not allow connecting anonymously / without accounts * python3 only * slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster) + * those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav known client bugs: * on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2 @@ -2614,9 +2615,16 @@ NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/ the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading) +if you want to sync with `u2c.py` then: +* the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into +* ...and u2c will not be able to sync files/folders which are affected by the global-options `no-hash` and/or `no-idx`, and the same goes for volflags `nohash` and/or `noidx` +* ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a` + * quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync + alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare * starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections + * but this is only true for the initial upload; u2c will be faster for periodic syncing ## mount as drive diff --git a/bin/README.md b/bin/README.md index fd94172f..1fef13f4 100644 --- a/bin/README.md +++ b/bin/README.md @@ -1,7 +1,7 @@ # [`u2c.py`](u2c.py) * command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm) * file uploads, file-search, autoresume of aborted/broken uploads -* sync local folder to server +* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync) * generally faster than browsers * if something breaks just restart it From 59f142cd19b46d7b692d0762f35e4d80bc4f88d9 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 23 Aug 2025 19:58:33 +0000 Subject: [PATCH 090/154] readme: u2c: rephrase no-hash warning --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e13f6bf3..c1a06d01 100644 --- a/README.md +++ b/README.md @@ -2617,7 +2617,7 @@ the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudst if you want to sync with `u2c.py` then: * the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into -* ...and u2c will not be able to sync files/folders which are affected by the global-options `no-hash` and/or `no-idx`, and the same goes for volflags `nohash` and/or `noidx` +* ...but DON'T enable global-options `no-hash` or `no-idx` (or volflags `nohash` / `noidx`), or at least make sure they are configured so they do not affect anything you are syncing into * ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a` * quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync From 4b8c22159a4d84a28013e00519250b1e3535b7c0 Mon Sep 17 00:00:00 2001 From: 4ntsy <228124712+4ntsy@users.noreply.github.com> Date: Sat, 23 Aug 2025 18:28:37 -0300 Subject: [PATCH 091/154] Portuguese translation (#673) --- copyparty/web/browser.js | 632 ++++++++++++++++++++++++++++++++++++++- copyparty/web/splash.js | 47 +++ 2 files changed, 678 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 5f3fbb1e..bb2601d6 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -8199,6 +8199,636 @@ var Ls = { "lang_set": "odśwież stronę (F5), aby zastosować zmianę.", }, + "por": { + "tt": "Português", + + "cols": { + "c": "botões de ação", + "dur": "duração", + "q": "qualidade / bitrate", + "Ac": "codec de áudio", + "Vc": "codec de vídeo", + "Fmt": "formato / contêiner", + "Ahash": "checksum de áudio", + "Vhash": "checksum de vídeo", + "Res": "resolução", + "T": "tipo de arquivo", + "aq": "qualidade / bitrate de áudio", + "vq": "qualidade / bitrate de vídeo", + "pixfmt": "subamostragem / estrutura de pixel", + "resw": "resolução horizontal", + "resh": "resolução vertical", + "chs": "canais de áudio", + "hz": "taxa de amostragem" + }, + + "hks": [ + [ + "diversos", + ["ESC", "fechar várias coisas"], + + "gerenciador de arquivos", + ["G", "alternar entre visualização de lista / grade"], + ["T", "alternar entre miniaturas / ícones"], + ["⇧ A/D", "tamanho da miniatura"], + ["ctrl-K", "excluir selecionados"], + ["ctrl-X", "recortar seleção para a área de transferência"], + ["ctrl-C", "copiar seleção para a área de transferência"], + ["ctrl-V", "colar (mover/copiar) aqui"], + ["Y", "baixar selecionado"], + ["F2", "renomear selecionado"], + + "seleção de lista de arquivos", + ["espaço", "alternar seleção de arquivo"], + ["↑/↓", "mover cursor de seleção"], + ["ctrl ↑/↓", "mover cursor e visualização"], + ["⇧ ↑/↓", "selecionar arquivo anterior/próximo"], + ["ctrl-A", "selecionar todos os arquivos / pastas"], + ], [ + "navegação", + ["B", "alternar entre breadcrumbs / painel de navegação"], + ["I/K", "pasta anterior/próxima"], + ["M", "pasta pai (ou desexpandir a atual)"], + ["V", "alternar entre pastas / arquivos de texto no painel de navegação"], + ["A/D", "tamanho do painel de navegação"], + ], [ + "reprodutor de áudio", + ["J/L", "música anterior/próxima"], + ["U/O", "pular 10 segundos para trás/frente"], + ["0..9", "pular para 0%..90%"], + ["P", "reproduzir/pausar (também inicia)"], + ["S", "selecionar a música que está tocando"], + ["Y", "baixar música"], + ], [ + "visualizador de imagens", + ["J/L, ←/→", "imagem anterior/próxima"], + ["Home/End", "primeira/última imagem"], + ["F", "tela cheia"], + ["R", "girar no sentido horário"], + ["⇧ R", "girar no sentido anti-horário"], + ["S", "selecionar imagem"], + ["Y", "baixar imagem"], + ], [ + "reprodutor de vídeo", + ["U/O", "pular 10 segundos para trás/frente"], + ["P/K/Espaço", "reproduzir/pausar"], + ["C", "continuar reproduzindo o próximo"], + ["V", "loop"], + ["M", "mudo"], + ["[ e ]", "definir intervalo de loop"], + ], [ + "visualizador de arquivos de texto", + ["I/K", "arquivo anterior/próximo"], + ["M", "fechar arquivo de texto"], + ["E", "editar arquivo de texto"], + ["S", "selecionar arquivo (para recortar/copiar/renomear)"], + ] + ], + + "m_ok": "OK", + "m_ng": "Cancelar", + + "enable": "Ativar", + "danger": "PERIGO", + "clipped": "copiado para a área de transferência", + + "ht_s1": "segundo", + "ht_s2": "segundos", + "ht_m1": "minuto", + "ht_m2": "minutos", + "ht_h1": "hora", + "ht_h2": "horas", + "ht_d1": "dia", + "ht_d2": "dias", + "ht_and": " e ", + + "goh": "painel de controle", + "gop": 'pai anterior">anterior', + "gou": 'pasta pai">acima', + "gon": 'próxima pasta">próximo', + "logout": "Sair ", + "access": " acesso", + "ot_close": "fechar submenu", + "ot_search": "procurar arquivos por atributos, caminho / nome, tags de música ou qualquer combinação deles$N$N<code>foo bar</code> = deve conter ambos «foo» e «bar»,$N<code>foo -bar</code> = deve conter «foo» mas não «bar»,$N<code>^yana .opus$</code> = começar com «yana» e ser um arquivo «opus»$N<code>"try unite"</code> = conter exatamente «try unite»$N$No formato de data é iso-8601, como$N<code>2009-12-31</code> ou <code>2020-09-12 23:30:00</code>", + "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-md: criar um novo documento markdown", + "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", + "ot_u2i": 'up2k: fazer upload de arquivos (se você tiver acesso de escrita) ou alternar para o modo de busca para ver se eles já existem em algum lugar no servidor$N$Nuploads são reiniciáveis, multithread, e os carimbos de data/hora dos arquivos são preservados, mas usa mais CPU que [🎈]  (o uploader básico)<br /><br />durante os uploads, este ícone se torna um indicador de progresso!', + "ot_u2w": 'up2k: fazer upload de arquivos com suporte a retomada (feche seu navegador e solte os mesmos arquivos mais tarde)$N$Nmultithread, e os carimbos de data/hora dos arquivos são preservados, mas usa mais CPU que [🎈]  (o uploader básico)<br /><br />durante os uploads, este ícone se torna um indicador de progresso!', + "ot_noie": 'Por favor, use Chrome / Firefox / Edge', + + "ab_mkdir": "criar diretório", + "ab_mkdoc": "novo documento markdown", + "ab_msg": "enviar msg para o log do srv", + + "ay_path": "pular para pastas", + "ay_files": "pular para arquivos", + + "wt_ren": "renomear itens selecionados$NHotkey: F2", + "wt_del": "excluir itens selecionados$NHotkey: ctrl-K", + "wt_cut": "recortar itens selecionados <small>(depois colar em outro lugar)</small>$NHotkey: ctrl-X", + "wt_cpy": "copiar itens selecionados para a área de transferência$N(para colá-los em outro lugar)$NHotkey: ctrl-C", + "wt_pst": "colar uma seleção previamente recortada / copiada$NHotkey: ctrl-V", + "wt_selall": "selecionar todos os arquivos$NHotkey: ctrl-A (quando o arquivo estiver em foco)", + "wt_selinv": "inverter seleção", + "wt_zip1": "baixar esta pasta como um arquivo compactado", + "wt_selzip": "baixar seleção como um arquivo compactado", + "wt_seldl": "baixar seleção como arquivos separados$NHotkey: Y", + "wt_npirc": "copiar informações da faixa em formato irc", + "wt_nptxt": "copiar informações da faixa em texto simples", + "wt_m3ua": "adicionar à playlist m3u (clique em <code>📻copiar</code> depois)", + "wt_m3uc": "copiar playlist m3u para a área de transferência", + "wt_grid": "alternar entre visualização de grade / lista$NHotkey: G", + "wt_prev": "faixa anterior$NHotkey: J", + "wt_play": "reproduzir / pausar$NHotkey: P", + "wt_next": "próxima faixa$NHotkey: L", + + "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", + "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", + "ul_btn": "soltar arquivos / pastas<br>aqui (ou clique em mim)", + "ul_btnu": "U P L O A D", + "ul_btns": "B U S C A R", + + "ul_hash": "hash", + "ul_send": "enviar", + "ul_done": "feito", + "ul_idle1": "nenhum upload está na fila ainda", + "ut_etah": "velocidade média de <em>hash</em>, e tempo estimado até o fim", + "ut_etau": "velocidade média de <em>upload</em> e tempo estimado até o fim", + "ut_etat": "velocidade média <em>total</em> e tempo estimado até o fim", + + "uct_ok": "concluído com sucesso", + "uct_ng": "ruim: falhou / rejeitado / não encontrado", + "uct_done": "ok e ruim combinados", + "uct_bz": "fazendo hash ou upload", + "uct_q": "ocioso, pendente", + + "utl_name": "nome do arquivo", + "utl_ulist": "lista", + "utl_ucopy": "copiar", + "utl_links": "links", + "utl_stat": "status", + "utl_prog": "progresso", + + // mantenha curto: + "utl_404": "404", + "utl_err": "ERRO", + "utl_oserr": "Erro-SO", + "utl_found": "encontrado", + "utl_defer": "adiar", + "utl_yolo": "YOLO", + "utl_done": "feito", + + "ul_flagblk": "os arquivos foram adicionados à fila</b><br>no entanto, há um up2k ocupado em outra aba do navegador,<br>então esperando que ele termine primeiro", + "ul_btnlk": "a configuração do servidor bloqueou este interruptor neste estado", + + "udt_up": "Upload", + "udt_srch": "Buscar", + "udt_drop": "solte aqui", + + "u_nav_m": '<h6>certo, o que você tem?</h6><code>Enter</code> = Arquivos (um ou mais)\n<code>ESC</code> = Uma pasta (incluindo subpastas)', + "u_nav_b": '<a href="#" id="modal-ok">Arquivos</a><a href="#" id="modal-ng">Uma pasta</a>', + + "cl_opts": "interruptores", + "cl_themes": "tema", + "cl_langs": "idioma", + "cl_ziptype": "download de pasta", + "cl_uopts": "interruptores up2k", + "cl_favico": "favicon", + "cl_bigdir": "grandes dirs", + "cl_hsort": "#sort", + "cl_keytype": "notação de tecla", + "cl_hiddenc": "colunas ocultas", + "cl_hidec": "ocultar", + "cl_reset": "resetar", + "cl_hpick": "toque nos cabeçalhos das colunas para ocultá-los na tabela abaixo", + "cl_hcancel": "ocultar coluna abortado", + + "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_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', + "ct_dir1st": 'ordenar pastas antes de arquivos">📁 primeiro', + "ct_nsort": 'ordem natural (para nomes de arquivos com dígitos iniciais)">nsort', + "ct_utc": 'mostrar todas as datas/horas em UTC">UTC', + "ct_readme": 'mostrar README.md nas listas de pastas">📜 readme', + "ct_idxh": 'mostrar index.html em vez de lista de pastas">htm', + "ct_sbars": 'mostrar barras de rolagem">⟊', + + "cut_umod": "se um arquivo já existe no servidor, atualizar o carimbo de data/hora de última modificação do servidor para corresponder ao seu arquivo local (requer permissões de escrita+exclusão)\">re📅", + + "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 <em>"este arquivo tem o mesmo tamanho no servidor?"</em> 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 <em>teoricamente</em> 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_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_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", + + "cft_text": "texto do favicon (deixe em branco e atualize para desativar)", + "cft_fg": "cor do primeiro plano", + "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_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", + + "tt_entree": "mostrar painel de navegação (árvore de diretórios)$NHotkey: B", + "tt_detree": "mostrar breadcrumbs$NHotkey: B", + "tt_visdir": "rolar para a pasta selecionada", + "tt_ftree": "alternar entre árvore de pastas / arquivos de texto$NHotkey: V", + "tt_pdock": "mostrar pastas pai em um painel acoplado no topo", + "tt_dynt": "crescer automaticamente à medida que a árvore se expande", + "tt_wrap": "quebra de linha", + "tt_hover": "revelar linhas transbordando ao passar o mouse$N( quebra a rolagem a menos que o cursor do mouse $N  esteja na margem esquerda )", + + "ml_pmode": "ao final da pasta...", + "ml_btns": "comandos", + "ml_tcode": "transcodificar", + "ml_tcode2": "transcodificar para", + "ml_tint": "matiz", + "ml_eq": "equalizador de áudio", + "ml_drc": "compressor de faixa dinâmica", + + "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_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 <b>não confiáveis</b>,$N❌ <b>desativar</b> em conexões lentas provavelmente\">full", + "mt_fau": "em telefones, evitar que a música pare se a próxima música não pré-carregar rápido o suficiente (pode fazer as tags aparecerem com falhas)\">☕️", + "mt_waves": "barra de busca de forma de onda:$Nmostrar amplitude de áudio no scrubber\">~s", + "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_oscv": "mostrar capa do álbum no osd\">art", + "mt_follow": "manter a faixa que está tocando rolando à vista\">🎯", + "mt_compact": "controles compactos\">⟎", + "mt_uncache": "limpar cache  (tente isso se seu navegador armazenou em cache$Numa cópia quebrada de uma música e se recusa a tocar)\">uncache", + "mt_mloop": "loop na pasta aberta\">🔁 loop", + "mt_mnext": "carregar a próxima pasta e continuar\">📂 próximo", + "mt_mstop": "parar reprodução\">⏸ parar", + "mt_cflac": "converter flac / wav para {0}\">flac", + "mt_caac": "converter aac / m4a para {0}\">aac", + "mt_coth": "converter todos os outros (não mp3) para {0}\">oth", + "mt_c2opus": "melhor escolha para desktops, laptops, android\">opus", + "mt_c2owa": "opus-weba, para iOS 17.5 e mais recentes\">owa", + "mt_c2caf": "opus-caf, para iOS 11 a 17\">caf", + "mt_c2mp3": "use isso em dispositivos muito antigos\">mp3", + "mt_c2flac": "melhor qualidade de som, mas downloads enormes\">flac", + "mt_c2wav": "reprodução não comprimida (ainda maior)\">wav", + "mt_c2ok": "legal, boa escolha", + "mt_c2nd": "esse não é o formato de saída recomendado para o seu dispositivo, mas tudo bem", + "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)", + + "mb_play": "reproduzir", + "mm_hashplay": "reproduzir este arquivo de áudio?", + "mm_m3u": "pressione <code>Enter/OK</code> para Reproduzir\npressione <code>ESC/Cancelar</code> para Editar", + "mp_breq": "precisa do firefox 82+ ou chrome 73+ ou iOS 15+", + "mm_bload": "carregando...", + "mm_bconv": "convertendo para {0}, por favor, espere...", + "mm_opusen": "seu navegador não pode reproduzir arquivos aac / m4a;\na transcodificação para opus agora está ativada", + "mm_playerr": "reprodução falhou: ", + "mm_eabrt": "A tentativa de reprodução foi cancelada", + "mm_enet": "Sua conexão de internet está instável", + "mm_edec": "Este arquivo está supostamente corrompido??", + "mm_esupp": "Seu navegador não entende este formato de áudio", + "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_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", + "mm_prescan": "Procurando música para tocar a seguir...", + "mm_scank": "Encontrei a próxima música:", + "mm_uncache": "cache limpo; todas as músicas serão baixadas novamente na próxima reprodução", + "mm_hnf": "essa música não existe mais", + + "im_hnf": "essa imagem não existe mais", + + "f_empty": 'esta pasta está vazia', + "f_chide": 'isso irá ocultar a coluna «{0}»\n\nvocê pode reexibir as colunas na aba de configurações', + "f_bigtxt": "este arquivo tem {0} MiB de tamanho -- realmente ver como texto?", + "f_bigtxt2": "ver apenas o final do arquivo em vez disso? isso também ativará o acompanhamento/tailing, mostrando linhas de texto recém-adicionadas em tempo real", + "fbd_more": '<div id="blazy">mostrando <code>{0}</code> de <code>{1}</code> arquivos; <a href="#" id="bd_more">mostrar {2}</a> ou <a href="#" id="bd_all">mostrar todos</a></div>', + "fbd_all": '<div id="blazy">mostrando <code>{0}</code> de <code>{1}</code> arquivos; <a href="#" id="bd_all">mostrar todos</a></div>', + "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_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 <code>.PARTIAL</code>. Por favor, pressione CANCELAR ou Escape para fazer isso.\n\nPressionar OK / Enter irá ignorar este aviso e continuar baixando o arquivo temporário <code>.PARTIAL</code>, o que quase certamente lhe dará dados corrompidos.", + + "ft_paste": "colar {0} itens$NHotkey: ctrl-V", + "fr_eperm": 'não é possível renomear:\nvocê não tem permissão de “mover” nesta pasta', + "fd_eperm": 'não é possível excluir:\nvocê não tem permissão de “excluir” nesta pasta', + "fc_eperm": 'não é possível recortar:\nvocê não tem permissão de “mover” nesta pasta', + "fp_eperm": 'não é possível colar:\nvocê não tem permissão de “escrever” nesta pasta', + "fr_emore": "selecione pelo menos um item para renomear", + "fd_emore": "selecione pelo menos um item para excluir", + "fc_emore": "selecione pelo menos um item para recortar", + "fcp_emore": "selecione pelo menos um item para copiar para a área de transferência", + + "fs_sc": "compartilhar a pasta em que você está", + "fs_ss": "compartilhar os arquivos selecionados", + "fs_just1d": "você não pode selecionar mais de uma pasta,\nou misturar arquivos e pastas em uma seleção", + "fs_abrt": "❌ abortar", + "fs_rand": "🎲 nome aleatório", + "fs_go": "✅ criar compartilhamento", + "fs_name": "nome", + "fs_src": "fonte", + "fs_pwd": "senha", + "fs_exp": "expira", + "fs_tmin": "min", + "fs_thrs": "horas", + "fs_tdays": "dias", + "fs_never": "eterno", + "fs_pname": "nome do link opcional; será aleatório se em branco", + "fs_tsrc": "o arquivo ou pasta a ser compartilhado", + "fs_ppwd": "senha opcional", + "fs_w8": "criando compartilhamento...", + "fs_ok": "pressione <code>Enter/OK</code> para Copiar para a Área de Transferência\npressione <code>ESC/Cancelar</code> para Fechar", + + "frt_dec": "pode consertar alguns casos de nomes de arquivos quebrados\">url-decode", + "frt_rst": "resetar nomes de arquivos modificados de volta para os originais\">↺ resetar", + "frt_abrt": "abortar e fechar esta janela\">❌ cancelar", + "frb_apply": "APLICAR RENOMEAÇÃO", + "fr_adv": "renomeação em lote / metadados / padrão\">avançado", + "fr_case": "regex sensível a maiúsculas e minúsculas\">case", + "fr_win": "nomes seguros para windows; substituir <code><>:"\\|?*</code> por caracteres japoneses de largura total\">win", + "fr_slash": "substituir <code>/</code> por um caractere que não cause a criação de novas pastas\">no /", + "fr_re": "padrão de busca regex para aplicar aos nomes de arquivos originais; grupos de captura podem ser referenciados no campo de formato abaixo como <code>(1)</code> e <code>(2)</code> e assim por diante", + "fr_fmt": "inspirado por foobar2000:$N<code>(título)</code> é substituído pelo título da música,$N<code>[(artista) - ](título)</code> pula esta parte se o artista estiver em branco$N<code>$lpad((tn),2,0)</code> preenche o número da faixa com 2 dígitos", + "fr_pdel": "excluir", + "fr_pnew": "salvar como", + "fr_pname": "forneça um nome para seu novo preset", + "fr_aborted": "abortado", + "fr_lold": "nome antigo", + "fr_lnew": "novo nome", + "fr_tags": "tags para os arquivos selecionados (somente leitura, apenas para referência):", + "fr_busy": "renomeando {0} itens...\n\n{1}", + "fr_efail": "renomeação falhou:\n", + "fr_nchg": "{0} dos novos nomes foram alterados devido a <code>win</code> e/ou <code>no /</code>\n\nOK para continuar com estes novos nomes alterados?", + + "fd_ok": "exclusão OK", + "fd_err": "exclusão falhou:\n", + "fd_none": "nada foi excluído; talvez bloqueado pela configuração do servidor (xbd)?", + "fd_busy": "excluindo {0} itens...\n\n{1}", + "fd_warn1": "EXCLUIR estes {0} itens?", + "fd_warn2": "<b>Última chance!</b> Não há como desfazer. Excluir?", + + "fc_ok": "recortar {0} itens", + "fc_warn": 'recortar {0} itens\n\nmas: apenas <b>esta</b> aba do navegador pode colá-los\n(já que a seleção é tão absolutamente massiva)', + + "fcc_ok": "copiado {0} itens para a área de transferência", + "fcc_warn": 'copiado {0} itens para a área de transferência\n\nmas: apenas <b>esta</b> aba do navegador pode colá-los\n(já que a seleção é tão absolutamente massiva)', + + "fp_apply": "usar estes nomes", + "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 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 para pular:", + "fp_emore": "ainda há algumas colisões de nome de arquivo para consertar", + "fp_ok": "movimento OK", + "fcp_ok": "cópia OK", + "fp_busy": "movendo {0} itens...\n\n{1}", + "fcp_busy": "copiando {0} itens...\n\n{1}", + "fp_abrt": "abortando...", + "fp_err": "movimento falhou:\n", + "fcp_err": "cópia falhou:\n", + "fp_confirm": "mover estes {0} itens para cá?", + "fcp_confirm": "copiar estes {0} itens para cá?", + "fp_etab": 'falha ao ler a área de transferência de outra aba do navegador', + "fp_name": "enviando um arquivo do seu dispositivo. Dê-lhe um nome:", + "fp_both_m": '<h6>escolha o que colar</h6><code>Enter</code> = Mover {0} arquivos de «{1}»\n<code>ESC</code> = Enviar {2} arquivos do seu dispositivo', + "fcp_both_m": '<h6>escolha o que colar</h6><code>Enter</code> = Copiar {0} arquivos de «{1}»\n<code>ESC</code> = Enviar {2} arquivos do seu dispositivo', + "fp_both_b": '<a href="#" id="modal-ok">Mover</a><a href="#" id="modal-ng">Enviar</a>', + "fcp_both_b": '<a href="#" id="modal-ok">Copiar</a><a href="#" id="modal-ng">Enviar</a>', + + "mk_noname": "digite um nome no campo de texto à esquerda antes de fazer isso :p", + + "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 ", + "tv_xe2": "404, arquivo não encontrado", + "tv_lst": "lista de arquivos de texto em", + "tvt_close": "voltar para a visualização da pasta$NHotkey: M (ou Esc)\">❌ fechar", + "tvt_dl": "baixar este arquivo$NHotkey: Y\">💾 baixar", + "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_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\">↵", + "tvt_atail": "fixar rolagem no final da página\">⚓", + "tvt_ctail": "decodificar cores do terminal (códigos de escape ansi)\">🌈", + "tvt_ntail": "limite de rolagem para trás (quantos bytes de texto manter carregados)", + + "m3u_add1": "música adicionada à playlist m3u", + "m3u_addn": "{0} músicas adicionadas à playlist m3u", + "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_crop": "cortar miniaturas ao centro\">cortar", + "gt_3x": "miniaturas de alta resolução\">3x", + "gt_zoom": "zoom", + "gt_chop": "picar", + "gt_sort": "ordenar por", + "gt_name": "nome", + "gt_sz": "tamanho", + "gt_ts": "data", + "gt_ext": "tipo", + "gt_c1": "truncar nomes de arquivos mais (mostrar menos)", + "gt_c2": "truncar nomes de arquivos menos (mostrar mais)", + + "sm_w8": "buscando...", + "sm_prev": "os resultados da busca abaixo são de uma consulta anterior:\n ", + "sl_close": "fechar resultados da busca", + "sl_hits": "mostrando {0} resultados", + "sl_moar": "carregar mais", + + "s_sz": "tamanho", + "s_dt": "data", + "s_rd": "caminho", + "s_fn": "nome", + "s_ta": "tags", + "s_ua": "up@", + "s_ad": "adv.", + "s_s1": "MiB mínimo", + "s_s2": "MiB máximo", + "s_d1": "iso8601 min.", + "s_d2": "iso8601 max.", + "s_u1": "enviado depois de", + "s_u2": "e/ou antes de", + "s_r1": "caminho contém   (separado por espaço)", + "s_f1": "nome contém   (negar com -nope)", + "s_t1": "tags contém   (^=início, fim=$)", + "s_a1": "propriedades de metadados específicas", + + "md_eshow": "não é possível renderizar ", + "md_off": "[📜<em>readme</em>] desativado em [⚙️] -- documento oculto", + + "badreply": "Falha ao analisar a resposta do servidor", + + "xhr403": "403: Acesso negado\n\ntente pressionar F5, talvez você tenha saído da conta", + "xhr0": "desconhecido (provavelmente perdeu a conexão com o servidor, ou o servidor está offline)", + "cf_ok": "desculpe por isso -- a proteção DD" + "oS foi ativada\n\nas coisas devem ser retomadas em cerca de 30 segundos\n\nse nada acontecer, pressione F5 para recarregar a página", + "tl_xe1": "não foi possível listar as subpastas:\n\nerro ", + "tl_xe2": "404: Pasta não encontrada", + "fl_xe1": "não foi possível listar os arquivos na pasta:\n\nerro ", + "fl_xe2": "404: Pasta não encontrada", + "fd_xe1": "não foi possível criar a subpasta:\n\nerro ", + "fd_xe2": "404: Pasta pai não encontrada", + "fsm_xe1": "não foi possível enviar a mensagem:\n\nerro ", + "fsm_xe2": "404: Pasta pai não encontrada", + "fu_xe1": "falha ao carregar a lista de despublicação do servidor:\n\nerro ", + "fu_xe2": "404: Arquivo não encontrado??", + + "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_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)", + + "un_m1": "você pode excluir seus uploads recentes (ou abortar os que não foram concluídos) abaixo", + "un_upd": "atualizar", + "un_m4": "ou compartilhar os arquivos visíveis abaixo:", + "un_ulist": "mostrar", + "un_ucopy": "copiar", + "un_flt": "filtro opcional:  a URL deve conter", + "un_fclr": "limpar filtro", + "un_derr": 'a exclusão da despublicação falhou:\n', + "un_f5": 'algo quebrou, por favor, tente uma atualização ou pressione F5', + "un_uf5": "desculpe, mas você tem que atualizar a página (por exemplo, pressionando F5 ou CTRL-R) antes que este upload possa ser abortado", + "un_nou": "<b>aviso:</b> o servidor está muito ocupado para mostrar uploads incompletos; clique no link \"atualizar\" em um momento", + "un_noc": "<b>aviso:</b> a despublicação de arquivos totalmente enviados não está ativada/permitida na configuração do servidor", + "un_max": "mostrando os primeiros 2000 arquivos (use o filtro)", + "un_avail": "{0} uploads recentes podem ser excluídos<br />{1} incompletos podem ser abortados", + "un_m2": "ordenado por tempo de upload; o mais recente primeiro:", + "un_no1": "sike! nenhum upload é suficientemente recente", + "un_no2": "sike! nenhum upload que corresponda a esse filtro é suficientemente recente", + "un_next": "excluir os próximos {0} arquivos abaixo", + "un_abrt": "abortar", + "un_del": "excluir", + "un_m3": "carregando seus uploads recentes...", + "un_busy": "excluindo {0} arquivos...", + "un_clip": "{0} links copiados para a área de transferência", + + "u_https1": "você deveria", + "u_https2": "mudar para https", + "u_https3": "para um melhor desempenho", + "u_ancient": 'seu navegador é impressionantemente antigo -- talvez você devesse <a href="#" onclick="goto(\'bup\')">usar o bup em vez disso</a>', + "u_nowork": "precisa do firefox 53+ ou chrome 57+ ou iOS 11+", + "tail_2old": "precisa do firefox 105+ ou chrome 71+ ou iOS 14.5+", + "u_nodrop": 'seu navegador é muito antigo para upload de arrastar e soltar', + "u_notdir": "isso não é uma pasta!\n\nseu navegador é muito antigo,\npor favor, tente arrastar e soltar em vez disso", + "u_uri": "para arrastar e soltar imagens de outras janelas do navegador,\npor favor, solte-as no grande botão de upload", + "u_enpot": 'mudar para <a href="#">UI batata</a> (pode melhorar a velocidade de upload)', + "u_depot": 'mudar para <a href="#">UI chique</a> (pode reduzir a velocidade de upload)', + "u_gotpot": 'mudando para a UI batata para uma velocidade de upload melhorada,\n\nsinta-se à vontade para discordar e voltar!', + "u_pott": "<p>arquivos:   <b>{0}</b> concluídos,   <b>{1}</b> falhados,   <b>{2}</b> ocupados,   <b>{3}</b> na fila</p>", + "u_ever": "este é o uploader básico; up2k precisa de pelo menos<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", + "u_su2k": 'este é o uploader básico; <a href="#" id="u2yea">up2k</a> é melhor', + "u_uput": 'otimizar para velocidade (pular checksum)', + "u_ewrite": 'você não tem acesso de escrita a esta pasta', + "u_eread": 'você não tem acesso de leitura a esta pasta', + "u_enoi": 'a busca de arquivos não está ativada na configuração do servidor', + "u_enoow": "substituir não funcionará aqui; precisa de permissão de Excluir", + "u_badf": 'Estes {0} arquivos (de um total de {1}) foram ignorados, possivelmente devido a permissões do sistema de arquivos:\n\n', + "u_blankf": 'Estes {0} arquivos (de um total de {1}) estão em branco / vazios; enviá-los de qualquer maneira?\n\n', + "u_applef": 'Estes {0} arquivos (de um total de {1}) são provavelmente indesejáveis;\nPressione <code>OK/Enter</code> para PULAR os seguintes arquivos,\nPressione <code>Cancelar/ESC</code> para NÃO excluir, e ENVIAR esses também:\n\n', + "u_just1": '\nTalvez funcione melhor se você selecionar apenas um arquivo', + "u_ff_many": "se você estiver usando <b>Linux / MacOS / Android,</b> então essa quantidade de arquivos <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>pode</em> travar o Firefox!</a>\nse isso acontecer, por favor, tente novamente (ou use o Chrome).", + "u_up_life": "Este upload será excluído do servidor\n{0} após ser concluído", + "u_asku": 'enviar estes {0} arquivos para <code>{1}</code>', + "u_unpt": "você pode desfazer / excluir este upload usando o 🧯 no canto superior esquerdo", + "u_bigtab": 'prestes a mostrar {0} arquivos\n\nisto pode travar seu navegador, tem certeza?', + "u_scan": 'Escaneando arquivos...', + "u_dirstuck": 'o iterador de diretório travou ao tentar acessar os seguintes {0} itens; irá pular:', + "u_etadone": 'Concluído ({0}, {1} arquivos)', + "u_etaprep": '(preparando para enviar)', + "u_hashdone": 'hash concluído', + "u_hashing": 'hash', + "u_hs": 'handshaking...', + "u_started": "os arquivos estão sendo enviados agora; veja [🚀]", + "u_dupdefer": "duplicado; será processado após todos os outros arquivos", + "u_actx": "clique neste texto para evitar perda de<br />desempenho ao mudar para outras janelas/abas", + "u_fixed": "OK!  Consertado 👍", + "u_cuerr": "falha ao enviar o bloco {0} de {1};\nprovavelmente inofensivo, continuando\n\narquivo: {2}", + "u_cuerr2": "o servidor rejeitou o upload (bloco {0} de {1});\ntentará novamente mais tarde\n\narquivo: {2}\n\nerro ", + "u_ehstmp": "tentará novamente; veja no canto inferior direito", + "u_ehsfin": "o servidor rejeitou a solicitação para finalizar o upload; tentando novamente...", + "u_ehssrch": "o servidor rejeitou a solicitação para realizar a busca; tentando novamente...", + "u_ehsinit": "o servidor rejeitou a solicitação para iniciar o upload; tentando novamente...", + "u_eneths": "erro de rede ao realizar o handshake de upload; tentando novamente...", + "u_enethd": "erro de rede ao testar a existência do alvo; tentando novamente...", + "u_cbusy": "esperando o servidor confiar em nós novamente após uma falha de rede...", + "u_ehsdf": "o servidor ficou sem espaço em disco!\n\ncontinuará tentando novamente, caso alguém\nlibere espaço suficiente para continuar", + "u_emtleak1": "parece que seu navegador pode ter um vazamento de memória;\npor favor,", + "u_emtleak2": ' <a href="{0}">mude para https (recomendado)</a> ou ', + "u_emtleak3": ' ', + "u_emtleakc": 'tente o seguinte:\n<ul><li>pressione <code>F5</code> para atualizar a página</li><li>depois desative o botão  <code>mt</code>  nas  <code>⚙️ configurações</code></li><li>e tente o upload novamente</li></ul>Os uploads serão um pouco mais lentos, mas tudo bem.\nDesculpe pelo problema !\n\nPS: chrome v107 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1354816" target="_blank">tem uma correção de bug</a> para isso', + "u_emtleakf": 'tente o seguinte:\n<ul><li>pressione <code>F5</code> para atualizar a página</li><li>depois ative <code>🥔</code> (batata) na UI de upload<li>e tente o upload novamente</li></ul>\nPS: o firefox <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1790500" target="_blank">esperançosamente terá uma correção de bug</a> em algum momento', + "u_s404": "não encontrado no servidor", + "u_expl": "explicar", + "u_maxconn": "a maioria dos navegadores limita isso a 6, mas o firefox permite que você aumente com <code>connections-per-server</code> em <code>about:config</code>", + "u_tu": '<p class="warn">AVISO: turbo ativado, <span> o cliente pode não detectar e retomar uploads incompletos; veja a dica de ferramenta do botão turbo</span></p>', + "u_ts": '<p class="warn">AVISO: turbo ativado, <span> os resultados da busca podem estar incorretos; veja a dica de ferramenta do botão turbo</span></p>', + "u_turbo_c": "o turbo está desativado na configuração do servidor", + "u_turbo_g": "desativando o turbo porque você não tem\n privilégios de listagem de diretório neste volume", + "u_life_cfg": 'excluir automaticamente depois de <input id="lifem" p="60" /> min (ou <input id="lifeh" p="3600" /> horas)', + "u_life_est": 'o upload será excluído <span id="lifew" tt="local time">---</span>', + "u_life_max": 'esta pasta impõe um\n tempo de vida máximo de {0}', + "u_unp_ok": 'a despublicação é permitida para {0}', + "u_unp_ng": 'a despublicação NÃO será permitida', + "ue_ro": 'seu acesso a esta pasta é Somente Leitura\n\n', + "ue_nl": 'você não está logado no momento', + "ue_la": 'você está logado no momento como "{0}"', + "ue_sr": 'você está no modo de busca de arquivos no momento\n\nmude para o modo de upload clicando na lupa 🔎 (ao lado do grande botão BUSCAR), e tente enviar novamente\n\ndesculpe', + "ue_ta": 'tente enviar novamente, deve funcionar agora', + "ue_ab": "este arquivo já está sendo enviado para outra pasta, e esse upload deve ser concluído antes que o arquivo possa ser enviado para outro lugar.\n\nVocê pode abortar e esquecer o upload inicial usando o 🧯 no canto superior esquerdo", + "ur_1uo": "OK: Arquivo enviado com sucesso", + "ur_auo": "OK: Todos os {0} arquivos enviados com sucesso", + "ur_1so": "OK: Arquivo encontrado no servidor", + "ur_aso": "OK: Todos os {0} arquivos encontrados no servidor", + "ur_1un": "O upload falhou, desculpe", + "ur_aun": "Todos os {0} uploads falharam, desculpe", + "ur_1sn": "O arquivo NÃO foi encontrado no servidor", + "ur_asn": "Os {0} arquivos NÃO foram encontrados no servidor", + "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", + + "lang_set": "atualizar para a mudança ter efeito?" + }, "rus": { "tt": "Русский", @@ -10720,7 +11350,7 @@ var Ls = { }, }; -var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "kor", "nld", "rus", "spa", "swe", "ukr"]; +var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "kor", "nld", "por", "rus", "spa", "swe", "ukr"]; if (window.langmod) langmod(); diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index ad2d81db..4ff09fd1 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -571,6 +571,53 @@ var Ls = { "af1": "pokaż ostatnio przesłane pliki", "ag1": "pokaż znanych użytkowników IdP", }, + "por": { + "a1": "atualizar", + "b1": "olá   <small>(você não está logado)</small>", + "c1": "encerrar sessão", + "d1": "despejar o estado da pilha", + "d2": "mostra o estado de todos os threads ativos", + "e1": "recarregar configuração", + "e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito", + "f1": "você pode navegar:", + "g1": "você pode fazer upload para:", + "cc1": "outras coisas:", + "h1": "desativar k304", + "i1": "ativar k304", + "j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), <em>mas</em> também irá desacelerar as coisas em geral", + "k1": "redefinir config. de cliente", + "l1": "faça login para mais:", + "ls3": "fazer login", + "lu4": "nome de usuário", + "lp4": "senha", + "lo3": "encerrar sessão de \"{0}\" em todos os lugares", + "lo2": "isso irá encerrar a sessão em todos os navegadores", + "m1": "bem-vindo de volta,", + "n1": "404 não encontrado  ┐( ´ -`)┌", + "o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início", + "p1": "403 proibido  ~┻━┻", + "q1": "use uma senha ou volte para o início", + "r1": "ir para o início", + ".s1": "reescanear", + "t1": "ação", + "u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos", + "v1": "conectar", + "v2": "usar este servidor como um disco rígido local", + "w1": "mudar para https", + "x1": "mudar senha", + "y1": "editar recursos compartilhados", + "z1": "desbloquear este recurso compartilhado:", + "ta1": "primeiro digite sua nova senha", + "ta2": "repita para confirmar a nova senha:", + "ta3": "há um erro; por favor, tente novamente", + "aa1": "arquivos de entrada:", + "ab1": "desativar no304", + "ac1": "ativar no304", + "ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!", + "ae1": "downloads ativos:", + "af1": "mostrar uploads recentes", + "ag1": "mostrar usuários IdP conhecidos" + }, "spa": { "a1": "actualizar", "b1": "hola   <small>(no has iniciado sesión)</small>", From 8f235be66fa11526f40181658e9371ac19703e9b Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 23 Aug 2025 21:31:30 +0000 Subject: [PATCH 092/154] enable polish translation --- 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 bb2601d6..e86b3c54 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -11350,7 +11350,7 @@ var Ls = { }, }; -var LANGS = ["eng", "nor", "nno", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "kor", "nld", "por", "rus", "spa", "swe", "ukr"]; +var LANGS = ["eng", "nor", "chi", "cze", "deu", "fin", "fra", "grc", "ita", "kor", "nld", "nno", "pol", "por", "rus", "spa", "swe", "ukr"]; if (window.langmod) langmod(); From 0491123bb2595393bf85504a10da0d0ef718fcb6 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 23 Aug 2025 22:29:41 +0000 Subject: [PATCH 093/154] add markdown newlines option (#552) --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 3 ++- copyparty/cfg.py | 2 ++ copyparty/httpcli.py | 3 ++- copyparty/web/browser.js | 2 +- copyparty/web/md.html | 3 ++- copyparty/web/md.js | 2 +- copyparty/web/mde.html | 3 ++- tests/util.py | 2 +- 9 files changed, 14 insertions(+), 7 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3cb97949..120d3481 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1634,6 +1634,7 @@ def add_db_metadata(ap): def add_txt(ap): ap2 = ap.add_argument_group("textfile options") + ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)") ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)") ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)") ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index de8e764c..1b922c22 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2811,7 +2811,8 @@ class AuthSrv(object): "have_mv": not self.args.no_mv, "have_del": not self.args.no_del, "have_unpost": int(self.args.unpost), - "have_emp": self.args.emp, + "have_emp": int(self.args.emp), + "md_no_br": int(vf.get("md_no_br") or 0), "ext_th": vf.get("ext_th_d") or {}, "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"), "sba_md": vf.get("md_sba") or "", diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 4fa73e72..51c47853 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -44,6 +44,7 @@ def vf_bmap() -> dict[str, str]: "gsel", "hardlink", "magic", + "md_no_br", "no_db_ip", "no_sb_md", "no_sb_lg", @@ -324,6 +325,7 @@ flagcats = { "og_ua": "if defined: only send OG html if useragent matches this regex", }, "textfiles": { + "md_no_br": "newline only on double-newline or two tailing spaces", "md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope", "exp": "enable textfile expansion; see --help-exp", "exp_md": "placeholders to expand in markdown files; see --help", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 885a5fb9..695c5213 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -4933,7 +4933,8 @@ class HttpCli(object): "lastmod": int(ts_md * 1000), "lang": self.args.lang, "favico": self.args.favico, - "have_emp": self.args.emp, + "have_emp": int(self.args.emp), + "md_no_br": int(vn.flags.get("md_no_br") or 0), "md_chk_rate": self.args.mcr, "md": boundary, "arg_base": arg_base, diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index e86b3c54..fb6fea90 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -19292,7 +19292,7 @@ function show_md(md, name, div, url, depth) { var marked_opts = { headerPrefix: 'md-', - breaks: true, + breaks: !md_no_br, gfm: true }; var ext = md_plug.pre; diff --git a/copyparty/web/md.html b/copyparty/web/md.html index c3e2dfce..1dfed659 100644 --- a/copyparty/web/md.html +++ b/copyparty/web/md.html @@ -130,7 +130,8 @@ write markdown (most html is 🙆 too) var SR = "{{ r }}", last_modified = {{ lastmod }}, - have_emp = {{ "true" if have_emp else "false" }}, + have_emp = {{ have_emp }}, + md_no_br = {{ md_no_br }}, dfavico = "{{ favico }}"; var md_opt = { diff --git a/copyparty/web/md.js b/copyparty/web/md.js index 9ab5a2fc..d6064c9e 100644 --- a/copyparty/web/md.js +++ b/copyparty/web/md.js @@ -201,7 +201,7 @@ function convert_markdown(md_text, dest_dom) { var marked_opts = { //headerPrefix: 'h-', - breaks: true, + breaks: !md_no_br, gfm: true }; diff --git a/copyparty/web/mde.html b/copyparty/web/mde.html index adef6793..f6d0940f 100644 --- a/copyparty/web/mde.html +++ b/copyparty/web/mde.html @@ -28,7 +28,8 @@ var SR = "{{ r }}", last_modified = {{ lastmod }}, - have_emp = {{ "true" if have_emp else "false" }}, + have_emp = {{ have_emp }}, + md_no_br = {{ md_no_br }}, dfavico = "{{ favico }}"; var md_opt = { diff --git a/tests/util.py b/tests/util.py index 2318bab7..9b1a0e27 100644 --- a/tests/util.py +++ b/tests/util.py @@ -143,7 +143,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" + ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ka.update(**{k: False for k in ex.split()}) ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip" From 68503444c7d6f6f7fc6f5a41258cabf50568a9ab Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 23 Aug 2025 22:33:35 +0000 Subject: [PATCH 094/154] markdown: fix <code> in <a>; closes #552 --- copyparty/web/ui.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/copyparty/web/ui.css b/copyparty/web/ui.css index 212a77fa..7181bbab 100644 --- a/copyparty/web/ui.css +++ b/copyparty/web/ui.css @@ -430,6 +430,15 @@ html.y textarea:focus { .mdo code { font-size: .96em; } +html.z .mdo a>code, +html.y .mdo a>code { + color: inherit; + background: inherit; + background: rgba(0, 0, 0, 0.2); + padding-top: 0; + padding-bottom: 0; + border: none; +} .mdo h1, .mdo h2 { line-height: 1.5em; From 48d6224ec899b47b6a3509625af744fc60cc1903 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 24 Aug 2025 20:54:28 +0000 Subject: [PATCH 095/154] url-param "dl" downloads file --- copyparty/httpcli.py | 37 +++++++++++++++++-------------------- copyparty/util.py | 28 ++++++++++++++++++++++++++++ docs/devnotes.md | 1 + 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 695c5213..88a83105 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -12,7 +12,6 @@ import random import re import socket import stat -import string import sys import threading # typechk import time @@ -31,7 +30,7 @@ try: except: pass -from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode +from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode from .__version__ import S_VERSION from .authsrv import LEELOO_DALLAS, VFS # typechk from .bos import bos @@ -66,6 +65,7 @@ from .util import ( exclude_dotfiles, formatdate, fsenc, + gen_content_disposition, gen_filekey, gen_filekey_dbg, gencookie, @@ -4013,6 +4013,13 @@ class HttpCli(object): if not editions: return self.tx_404() + # + # force download + + if "dl" in self.ouparam: + cdis = gen_content_disposition(os.path.basename(req_path)) + self.out_headers["Content-Disposition"] = cdis + # # if-modified @@ -4181,6 +4188,13 @@ class HttpCli(object): if not editions: return self.tx_404() + # + # force download + + if "dl" in self.ouparam: + cdis = gen_content_disposition(os.path.basename(req_path)) + self.out_headers["Content-Disposition"] = cdis + # # if-modified @@ -4729,24 +4743,7 @@ class HttpCli(object): if maxn < nf: raise Pebkac(400, t) - safe = (string.ascii_letters + string.digits).replace("%", "") - afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn]) - bascii = unicode(safe).encode("utf-8") - zb = fn.encode("utf-8", "xmlcharrefreplace") - if not PY2: - zbl = [ - chr(x).encode("utf-8") - if x in bascii - else "%{:02x}".format(x).encode("ascii") - for x in zb - ] - else: - zbl = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in zb] - - ufn = b"".join(zbl).decode("ascii") - - cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}" - cdis = cdis.format(afn, ext, ufn, ext) + cdis = gen_content_disposition("%s.%s" % (fn, ext)) self.log(repr(cdis)) self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis}) diff --git a/copyparty/util.py b/copyparty/util.py index 97351f3c..76b68877 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -52,6 +52,7 @@ from .__init__ import ( VT100, WINDOWS, EnvParams, + unicode, ) from .__version__ import S_BUILD_DT, S_VERSION from .stolen import surrogateescape @@ -115,6 +116,10 @@ IP6ALL = "0:0:0:0:0:0:0:0" IP6_LL = ("fe8", "fe9", "fea", "feb") IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254") +UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._" +BC_CDISP = UC_CDISP.encode("ascii") +UC_CDISP_SET = set(UC_CDISP) +BC_CDISP_SET = set(BC_CDISP) try: import fcntl @@ -2073,6 +2078,29 @@ def gencookie( ) +def gen_content_disposition(fn: str) -> str: + safe = UC_CDISP_SET + bsafe = BC_CDISP_SET + fn = fn.replace("/", "_").replace("\\", "_") + zb = fn.encode("utf-8", "xmlcharrefreplace") + if not PY2: + zbl = [ + chr(x).encode("utf-8") + if x in bsafe + else "%{:02X}".format(x).encode("ascii") + for x in zb + ] + else: + zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb] + + ufn = b"".join(zbl).decode("ascii") + afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".") + while ".." in afn: + afn = afn.replace("..", ".") + + return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn) + + def humansize(sz: float, terse: bool = False) -> str: for unit in HUMANSIZE_UNITS: if sz < 1024: diff --git a/docs/devnotes.md b/docs/devnotes.md index 04c37764..5100471a 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -160,6 +160,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo` | method | params | result | |--|--|--| +| GET | `?dl` | download file (don't show in-browser) | | GET | `?ls` | list files/folders at URL as JSON | | GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles | | GET | `?ls=t` | list files/folders at URL as plaintext | From 35472557cb28c940a9e9a52723de8a077b1e75f4 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 24 Aug 2025 21:34:37 +0000 Subject: [PATCH 096/154] strongly prefer XDG_CONFIG_HOME; closes #442 --- copyparty/__main__.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 120d3481..bf392b1f 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -196,26 +196,33 @@ def init_E(EE: EnvParams) -> None: (unicode, "/tmp"), ] errs = [] - for chk in [os.listdir, os.mkdir]: + if True: for npath, (pf, pa) in enumerate(paths): p = "" try: p = pf(pa) - # print(chk.__name__, p, pa) if not p or p.startswith("~"): continue p = os.path.normpath(p) - chk(p) # type: ignore + if os.path.isdir(p) and os.listdir(p): + mkdir = False + else: + mkdir = True + os.mkdir(p) + p = os.path.join(p, "copyparty") if not os.path.isdir(p): os.mkdir(p) if npath > 1: - t = "Using [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" - errs.append(t % (p,)) + t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" + errs.append(t % (pa, p)) + elif mkdir: + t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)" + errs.append(t % (pa, p, " instead" if npath else "", pa)) elif errs: - errs.append("Using [%s] instead" % (p,)) + errs.append("Using %s/copyparty [%s] instead" % (pa, p)) if errs: warn(". ".join(errs)) @@ -223,8 +230,8 @@ def init_E(EE: EnvParams) -> None: return p # type: ignore except Exception as ex: if p and npath < 2: - t = "Unable to store config in [%s] due to %r" - errs.append(t % (p, ex)) + t = "Unable to store config in %s [%s] due to %r" + errs.append(t % (pa, p, ex)) raise Exception("could not find a writable path for config") From abffda5474cd9309c53fa0736395221c7c43bec1 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 24 Aug 2025 21:35:05 +0000 Subject: [PATCH 097/154] unindent after mkdir(XDG_CONFIG_HOME) --- copyparty/__main__.py | 61 +++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index bf392b1f..9dab68ce 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -196,42 +196,41 @@ def init_E(EE: EnvParams) -> None: (unicode, "/tmp"), ] errs = [] - if True: - for npath, (pf, pa) in enumerate(paths): - p = "" - try: - p = pf(pa) - if not p or p.startswith("~"): - continue + for npath, (pf, pa) in enumerate(paths): + p = "" + try: + p = pf(pa) + if not p or p.startswith("~"): + continue - p = os.path.normpath(p) - if os.path.isdir(p) and os.listdir(p): - mkdir = False - else: - mkdir = True - os.mkdir(p) + p = os.path.normpath(p) + if os.path.isdir(p) and os.listdir(p): + mkdir = False + else: + mkdir = True + os.mkdir(p) - p = os.path.join(p, "copyparty") - if not os.path.isdir(p): - os.mkdir(p) + p = os.path.join(p, "copyparty") + if not os.path.isdir(p): + os.mkdir(p) - if npath > 1: - t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" - errs.append(t % (pa, p)) - elif mkdir: - t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)" - errs.append(t % (pa, p, " instead" if npath else "", pa)) - elif errs: - errs.append("Using %s/copyparty [%s] instead" % (pa, p)) + if npath > 1: + t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" + errs.append(t % (pa, p)) + elif mkdir: + t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)" + errs.append(t % (pa, p, " instead" if npath else "", pa)) + elif errs: + errs.append("Using %s/copyparty [%s] instead" % (pa, p)) - if errs: - warn(". ".join(errs)) + if errs: + warn(". ".join(errs)) - return p # type: ignore - except Exception as ex: - if p and npath < 2: - t = "Unable to store config in %s [%s] due to %r" - errs.append(t % (pa, p, ex)) + return p # type: ignore + except Exception as ex: + if p and npath < 2: + t = "Unable to store config in %s [%s] due to %r" + errs.append(t % (pa, p, ex)) raise Exception("could not find a writable path for config") From 599e82f24d406ed048eb4bdf4db4e56fc9050481 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Mon, 25 Aug 2025 23:33:32 +0000 Subject: [PATCH 098/154] u2c: fix uploading files from root of unix fhs --- bin/u2c.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/u2c.py b/bin/u2c.py index e92c1e4e..f72e0474 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.11" -S_BUILD_DT = "2025-05-18" +S_VERSION = "2.12" +S_BUILD_DT = "2025-08-26" """ u2c.py: upload to copyparty @@ -10,7 +10,7 @@ u2c.py: upload to copyparty https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py - dependencies: no -- supports python 2.6, 2.7, and 3.3 through 3.12 +- supports python 2.6, 2.7, and 3.3 through 3.14 - if something breaks just try again and it'll autoresume """ @@ -677,7 +677,7 @@ def walkdirs(err, tops, excl): yield stop, ap[len(stop) :].lstrip(sep), inf else: d, n = top.rsplit(sep, 1) - yield d, n, os.stat(top) + yield d or b"/", n, os.stat(top) # mostly from copyparty/util.py @@ -1527,10 +1527,10 @@ def main(): # fmt: off ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog=""" -NOTE: -source file/folder selection uses rsync syntax, meaning that: +NOTE: source file/folder selection uses rsync syntax, meaning that: "foo" uploads the entire folder to URL/foo/ "foo/" uploads the CONTENTS of the folder into URL/ +NOTE: if server has --usernames enabled, then password is "username:password" """) ap.add_argument("url", type=unicode, help="server url, including destination folder") From d30240b431eaeccfc31a386c6b11fe70265264c1 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Tue, 26 Aug 2025 23:12:10 +0000 Subject: [PATCH 099/154] spaces in comma-sep. opts --- copyparty/__main__.py | 2 +- copyparty/authsrv.py | 1 + copyparty/cert.py | 1 + copyparty/pwhash.py | 1 + copyparty/svchub.py | 4 ++-- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 9dab68ce..26158879 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1974,7 +1974,7 @@ def main(argv: Optional[list[str]] = None) -> None: if not HAVE_IPV6 and al.i == "::": al.i = "0.0.0.0" - al.i = al.i.split(",") + al.i = [x.strip() for x in al.i.split(",")] try: if "-" in al.p: lo, hi = [int(x) for x in al.p.split("-")] diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 1b922c22..4309ba2c 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1659,6 +1659,7 @@ class AuthSrv(object): # accept both , and : as separators between usernames zs1, zs2 = x.replace("=", ":").split(":", 1) grps[zs1] = zs2.replace(":", ",").split(",") + grps[zs1] = [x.strip() for x in grps[zs1]] except: t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...' raise Exception(t.format(x)) diff --git a/copyparty/cert.py b/copyparty/cert.py index a22d2857..49fa9ea2 100644 --- a/copyparty/cert.py +++ b/copyparty/cert.py @@ -130,6 +130,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]): nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c) names = args.crt_ns.split(",") if args.crt_ns else [] + names = [x.strip() for x in names] if not args.crt_exact: for n in names[:]: names.append("*.{}".format(n)) diff --git a/copyparty/pwhash.py b/copyparty/pwhash.py index d4455ca8..d642d3c0 100644 --- a/copyparty/pwhash.py +++ b/copyparty/pwhash.py @@ -25,6 +25,7 @@ class PWHash(object): self.args = args zsl = args.ah_alg.split(",") + zsl = [x.strip() for x in zsl] alg = zsl[0] if alg == "none": alg = "" diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 199ccce9..5a5e16ec 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -325,7 +325,7 @@ class SvcHub(object): self._feature_test() - decs = {k: 1 for k in self.args.th_dec.split(",")} + decs = {k.strip(): 1 for k in self.args.th_dec.split(",")} if not HAVE_VIPS: decs.pop("vips", None) if not HAVE_PIL: @@ -1095,7 +1095,7 @@ class SvcHub(object): al.tcolor = "".join([x * 2 for x in al.tcolor]) zs = al.u2sz - zsl = zs.split(",") + zsl = [x.strip() for x in zs.split(",")] if len(zsl) not in (1, 3): t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)" raise Exception(t) From 543b7ea95938cb990d9c314951aed8375cfe2680 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Tue, 26 Aug 2025 23:33:53 +0000 Subject: [PATCH 100/154] add --auth-ord; closes #689 --- copyparty/__main__.py | 41 +++++++++++++++++++++++++++++++++++++++++ copyparty/authsrv.py | 13 +++++++++++++ copyparty/httpcli.py | 15 ++++++++++----- copyparty/web/splash.js | 2 +- tests/util.py | 1 + 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 26158879..37427fb1 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -676,6 +676,42 @@ def get_sects(): """ ), ], + [ + "auth-ord", + "authentication precedence", + dedent( + """ + \033[33m--auth-ord\033[0m is a comma-separated list of auth options + (one or more of the [\033[35moptions\033[0m] below); first one wins + + [\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header, + or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie + (see \033[33m--help-auth\033[0m) + + [\033[35midp\033[0m] is a username provided in the http-request-header + defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is + provided by an authentication middleware such as + authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m) + + [\033[35midp-h\033[0m] is specifically the \033[33m--idp-h-usr\033[0m header, + [\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header; + [\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m] + + [\033[35mipu\033[0m] is a mapping from an IP-address to a username, + auto-authing that client-IP to that account + (see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m) + + NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list, + it may still be enabled and can still take effect if + none of the other alternatives identify the user + + NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST + + NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button + will be hidden when any idp feature is enabled + """ + ), + ], [ "flags", "list of volflags", @@ -1254,6 +1290,7 @@ def add_auth(ap): ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP") ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)") + ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord") ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app") ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins") ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)") @@ -1264,6 +1301,10 @@ def add_auth(ap): ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]") ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-has-pw", type=u, default="", help=argparse.SUPPRESS) def add_chpw(ap): diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 4309ba2c..ff73f19a 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1713,6 +1713,7 @@ class AuthSrv(object): self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr) self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr) + self.setup_auth_ord() self.setup_pwhash(acct) defpw = acct.copy() @@ -2864,6 +2865,18 @@ class AuthSrv(object): zs = str(vol.flags.get("tcolor") or self.args.tcolor) vol.flags["tcolor"] = zs.lstrip("#") + def setup_auth_ord(self) -> None: + ao = [x.strip() for x in self.args.auth_ord.split(",")] + if "idp" in ao: + zi = ao.index("idp") + ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:] + zsl = "pw idp-h idp-hm ipu".split() + pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl] + self.args.ao_idp_before_pw = min(h, hm) < pw + self.args.ao_h_before_hm = h < hm + self.args.ao_ipu_wins = ipu == 0 + self.args.ao_have_pw = pw < 99 + def load_idp_db(self, quiet=False) -> None: # mutex me level = self.args.idp_store diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 88a83105..21676f33 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -624,7 +624,9 @@ class HttpCli(object): or "*" ) - if self.args.have_idp_hdrs: + if self.args.have_idp_hdrs and ( + self.uname == "*" or self.args.ao_idp_before_pw + ): idp_usr = "" if self.args.idp_hm_usr: for hn, hmv in self.args.idp_hm_usr_p.items(): @@ -637,9 +639,9 @@ class HttpCli(object): if idp_usr: break for hn in self.args.idp_h_usr: - if idp_usr: + if idp_usr and not self.args.ao_h_before_hm: break - idp_usr = self.headers.get(hn) + idp_usr = self.headers.get(hn) or idp_usr if idp_usr: idp_grp = ( self.headers.get(self.args.idp_h_grp) or "" @@ -688,7 +690,10 @@ class HttpCli(object): if idp_usr in self.asrv.vfs.aread: self.pw = "" self.uname = idp_usr - self.html_head += "<script>var is_idp=1</script>\n" + if self.args.ao_have_pw: + self.html_head += "<script>var is_idp=1</script>\n" + else: + self.html_head += "<script>var is_idp=2</script>\n" zs = self.asrv.ases.get(idp_usr) if zs: self.set_idp_cookie(zs) @@ -696,7 +701,7 @@ class HttpCli(object): self.log("unknown username: %r" % (idp_usr,), 1) if self.args.have_ipu_or_ipr: - if self.args.ipu and self.uname == "*": + if self.args.ipu and (self.uname == "*" or self.args.ao_ipu_wins): self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)] ipr = self.conn.hsrv.ipr if ipr and self.uname in ipr: diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 4ff09fd1..150b7dac 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -834,7 +834,7 @@ if (o1 && o2 && d.lo3) o1.setAttribute("value", d.lo3.format(o2.textContent)); try { - if (is_idp) { + if (is_idp > 1) { var z = ['#l+div', '#l', '#c']; for (var a = 0; a < z.length; a++) QS(z[a]).style.display = 'none'; diff --git a/tests/util.py b/tests/util.py index 9b1a0e27..06f5fd50 100644 --- a/tests/util.py +++ b/tests/util.py @@ -183,6 +183,7 @@ class Cfg(Namespace): v=v or [], c=c, E=E, + auth_ord="idp,ipu", bup_ck="sha512", chmod_d="755", cookie_cmax=8192, From 0b3939002de2eeab7f799673400218f60666febf Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 18:00:40 +0000 Subject: [PATCH 101/154] helptext tweaks --- copyparty/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 37427fb1..b8463a0d 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -693,7 +693,7 @@ def get_sects(): provided by an authentication middleware such as authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m) - [\033[35midp-h\033[0m] is specifically the \033[33m--idp-h-usr\033[0m header, + [\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header, [\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header; [\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m] @@ -1282,7 +1282,7 @@ def add_auth(ap): ses_db = os.path.join(E.cfg, "sessions.db") ap2 = ap.add_argument_group("IdP / identity provider / user authentication options") ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy") - ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp") + ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp") ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control") ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") From 0b50fde305887f3d9230e2ab6880c7c422901078 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 18:03:10 +0000 Subject: [PATCH 102/154] audio-thumb as folder-thumb; closes #691 --- copyparty/up2k.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 7ae6441b..13c57160 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -91,6 +91,9 @@ ICV_EXTS = set(zsg.split(",")) zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv" VCV_EXTS = set(zsg.split(",")) +zsg = "aif,aiff,alac,ape,flac,m4a,mp3,oga,ogg,opus,tak,tta,wav,wma,wv" +ACV_EXTS = set(zsg.split(",")) + zsg = "nohash noidx xdev xvol" VF_AFFECTS_INDEXING = set(zsg.split(" ")) @@ -1483,7 +1486,7 @@ class Up2k(object): unreg: list[str] = [] files: list[tuple[int, int, str]] = [] fat32 = True - cv = vcv = "" + cv = vcv = acv = "" th_cvd = self.args.th_coversd th_cvds = self.args.th_coversd_set @@ -1593,9 +1596,11 @@ class Up2k(object): cv = iname elif not vcv and ext in VCV_EXTS and not iname.startswith("."): vcv = iname + elif not acv and ext in ACV_EXTS and not iname.startswith("."): + acv = iname if not cv: - cv = vcv + cv = vcv or acv if not self.args.no_dirsz: tnf += len(files) From 4c042b3c8275ba0c40de8057cad20f3936466373 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 18:09:27 +0000 Subject: [PATCH 103/154] catch markdown table-fmt error; closes #699 --- copyparty/web/md2.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index 26b1e14b..a8229366 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -697,8 +697,14 @@ function reLastIndexOf(txt, ptn, end) { // table formatter function fmt_table(e) { if (e) e.preventDefault(); - //dom_tbox.className = ''; - + try { + fmt_table2(); + } + catch (ex) { + return toast.err(7, 'table-format (CTRL-K) failed:\n' + ex); + } +} +function fmt_table2() { var txt = dom_src.value, ofs = dom_src.selectionStart, //o0 = txt.lastIndexOf('\n\n', ofs), From f4f702c39dd2365f6ca43ed6b72a84c708fded8d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 18:56:23 +0000 Subject: [PATCH 104/154] shellcheck fixes; closes #690 a small selection of changes from the PR --- bin/bubbleparty.sh | 2 +- bin/prisonparty.sh | 2 +- copyparty/broker_util.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/bubbleparty.sh b/bin/bubbleparty.sh index 2e6aab3e..f59aaa02 100755 --- a/bin/bubbleparty.sh +++ b/bin/bubbleparty.sh @@ -9,7 +9,7 @@ bwrap \ --dev-bind /dev /dev \ --dir /tmp \ --dir /var \ - --bind $(pwd) $(pwd) \ + --bind "$(pwd)" "$(pwd)" \ --share-net \ --die-with-parent \ --file 11 /etc/passwd \ diff --git a/bin/prisonparty.sh b/bin/prisonparty.sh index d495c833..403f1588 100755 --- a/bin/prisonparty.sh +++ b/bin/prisonparty.sh @@ -141,7 +141,7 @@ chmod 777 "$jail/tmp" # create a dev -(cd $jail; mkdir -p dev; cd dev +(cd "$jail"; mkdir -p dev; cd dev [ -e null ] || mknod -m 666 null c 1 3 [ -e zero ] || mknod -m 666 zero c 1 5 [ -e random ] || mknod -m 444 random c 1 8 diff --git a/copyparty/broker_util.py b/copyparty/broker_util.py index ea987200..10c778dc 100644 --- a/copyparty/broker_util.py +++ b/copyparty/broker_util.py @@ -2,7 +2,6 @@ from __future__ import print_function, unicode_literals import argparse -import traceback from queue import Queue From 2848941e01476016e28bc3f44b1c647a7d96ff0d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 19:01:19 +0000 Subject: [PATCH 105/154] more qr tweaks (closes #533); * --qr-every draws the qr-code periodically, with/without --qr-pin * --qr-winch redraws --qr-pin on console size change * --qr-pin logs detected console size for debug --- copyparty/__main__.py | 2 ++ copyparty/svchub.py | 65 +++++++++++++++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index b8463a0d..783797af 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1157,6 +1157,8 @@ def add_qr(ap, tty): ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr") ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log") + ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)") + ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m") ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 5a5e16ec..50a3e2d4 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -134,6 +134,7 @@ class SvcHub(object): self.nsigs = 3 self.retcode = 0 self.httpsrv_up = 0 + self.qr_tsz = None self.log_mutex = threading.Lock() self.cday = 0 @@ -787,7 +788,27 @@ class SvcHub(object): self.signal_handler(signal.SIGTERM, None) def sticky_qr(self) -> None: - tw, th = termsize() + self._sticky_qr() + + def _unsticky_qr(self, flush=True) -> None: + print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="") + if flush: + sys.stderr.flush() + + def _sticky_qr(self, force: bool = False) -> None: + sz = termsize() + if self.qr_tsz == sz: + if not force: + return + else: + force = False + + if self.qr_tsz: + self._unsticky_qr(False) + else: + atexit.register(self._unsticky_qr) + + tw, th = self.qr_tsz = sz zs1, qr = self.tcpsrv.qr.split("\n", 1) url, colr = zs1.split(" ", 1) nl = len(qr.split("\n")) # numlines @@ -811,17 +832,34 @@ class SvcHub(object): url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url) qr = colr + qr - def unlock(): - print("\033[s\033[r\033[u", file=sys.stderr) - - atexit.register(unlock) t = "%s\033[%dA" % ("\n" * nl, nl) t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url) - self.pr(t, file=sys.stderr) + if not force: + self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6) + self.pr(t, file=sys.stderr, end="") - def sleepy_qr(self): - time.sleep(self.args.qr_wait) - self.log("qr-code", self.tcpsrv.qr) + def _qr_thr(self): + qr = self.tcpsrv.qr + w8 = self.args.qr_wait + if w8: + time.sleep(w8) + self.log("qr-code", qr) + w8 = self.args.qr_every + msg = "%s\033[%dA" % (qr, len(qr.split("\n"))) + while w8: + time.sleep(w8) + if self.stopping: + break + if self.args.qr_pin: + self._sticky_qr(True) + else: + self.log("qr-code", msg) + w8 = self.args.qr_winch + while w8: + time.sleep(w8) + if self.stopping: + break + self._sticky_qr() def cb_httpsrv_up(self) -> None: self.httpsrv_up += 1 @@ -837,11 +875,10 @@ class SvcHub(object): if self.tcpsrv.qr: if self.args.qr_pin: self.sticky_qr() - else: - if self.args.qr_wait: - Daemon(self.sleepy_qr, "qr_w8") - else: - self.log("qr-code", self.tcpsrv.qr) + if self.args.qr_wait or self.args.qr_every or self.args.qr_winch: + Daemon(self._qr_thr, "qr") + elif not self.args.qr_pin: + self.log("qr-code", self.tcpsrv.qr) else: self.log("root", "workers OK\n") From aa1c92130273716e8c7318bfd0e13aaa1f3fe39d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 19:17:36 +0000 Subject: [PATCH 106/154] support file-as-volume (#696); a volume can be a single file instead of a folder, but a misleading warning indicated otherwise --- README.md | 2 ++ copyparty/up2k.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index c1a06d01..a0b71d3a 100644 --- a/README.md +++ b/README.md @@ -572,6 +572,8 @@ for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mn the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead +> ℹ️ this also works for single files, because files can also be volumes + ## dotfiles diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 13c57160..1fc52333 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -926,6 +926,12 @@ class Up2k(object): with self.mutex, self.reg_mutex: # only need to protect register_vpath but all in one go feels right for vol in vols: + if bos.path.isfile(vol.realpath): + self.volstate[vol.vpath] = "online (just-a-file)" + t = "NOTE: volume [/%s] is a file, not a folder" + self.log(t % (vol.vpath,)) + continue + try: # mkdir gonna happen at snap anyways; bos.makedirs(vol.realpath, vf=vol.flags) From 28b93d796191d51765df1a43ae3241d7453f6c16 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 19:55:21 +0000 Subject: [PATCH 107/154] option to log invalid xml from clients (#695); windows webdav can send invalid xml in LOCK requests --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 15 ++++++++++++++- tests/util.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 783797af..944fa665 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1546,6 +1546,7 @@ def add_logging(ap): ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)") ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed") + ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 21676f33..171791a7 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -819,6 +819,15 @@ class HttpCli(object): 6 if em.startswith("client d/c ") else 3, ) + if self.hint and self.hint.startswith("<xml> "): + if self.args.log_badxml: + t = "invalid XML received from client: %r" + self.log(t % (self.hint[6:],), 6) + else: + t = "received invalid XML from client; enable --log-badxml to see the whole XML in the log" + self.log(t, 6) + self.hint = "" + msg = "%s\r\nURL: %s\r\n" % (em, self.vpath) if self.hint: msg += "hint: %s\r\n" % (self.hint,) @@ -1535,7 +1544,9 @@ class HttpCli(object): if not rbuf or len(buf) >= 32768: break - xroot = parse_xml(buf.decode(enc, "replace")) + sbuf = buf.decode(enc, "replace") + self.hint = "<xml> " + sbuf + xroot = parse_xml(sbuf) xtag = next((x for x in xroot if x.tag.split("}")[-1] == "prop"), None) if xtag is not None: props = set([y.tag.split("}")[-1] for y in xtag]) @@ -1741,6 +1752,7 @@ class HttpCli(object): uenc = enc.upper() txt = buf.decode(enc, "replace") + self.hint = "<xml> " + txt ET.register_namespace("D", "DAV:") xroot = mkenod("D:orz") xroot.insert(0, parse_xml(txt)) @@ -1801,6 +1813,7 @@ class HttpCli(object): uenc = enc.upper() txt = buf.decode(enc, "replace") + self.hint = "<xml> " + txt ET.register_namespace("D", "DAV:") lk = parse_xml(txt) assert lk.tag == "{DAV:}lockinfo" diff --git a/tests/util.py b/tests/util.py index 06f5fd50..a5674808 100644 --- a/tests/util.py +++ b/tests/util.py @@ -143,7 +143,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" + ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ka.update(**{k: False for k in ex.split()}) ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip" From d40f061a79f45ecade6af48b55753af04ac443d2 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 20:15:45 +0000 Subject: [PATCH 108/154] fix invalid up2k api usage --- copyparty/ftpd.py | 2 +- copyparty/smbd.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 197ca59d..b0eb8736 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -386,7 +386,7 @@ class FtpFs(AbstractedFS): svp = join(self.cwd, src).lstrip("/") dvp = join(self.cwd, dst).lstrip("/") try: - self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp) + self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp) except Exception as ex: raise FSE(str(ex)) diff --git a/copyparty/smbd.py b/copyparty/smbd.py index 2b9b3d77..7caec6cc 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -318,7 +318,7 @@ class SMB(object): t = "blocked rename (no-move-acc %s): /%s @%s" yeet(t % (vfs1.axs.umove, vp1, uname)) - self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2) + self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2) try: bos.makedirs(ap2, vf=vfs2.flags) except: From 200eaa92d7edd2e00bc821e9f862df3e51bc16ce Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 20:38:20 +0000 Subject: [PATCH 109/154] v1.19.6 --- copyparty/__version__.py | 4 ++-- docs/changelog.md | 28 ++++++++++++++++++++++++++++ scripts/help2html.py | 2 +- scripts/help2txt.sh | 2 +- scripts/prep.sh | 2 +- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 4703e216..8994d880 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 5) +VERSION = (1, 19, 6) CODENAME = "usernames" -BUILD_DT = (2025, 8, 21) +BUILD_DT = (2025, 8, 27) 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 8c4fbaf5..30d0fb30 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,31 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-0821-2319 `v1.19.5` it runs on iOS + +## 🧪 new features + +* #328 run copyparty on iPhones; see [install on iOS](https://github.com/9001/copyparty#install-on-iOS) in the readme ca98d54f + * cannot run in the background, doesn't have full access to your files, and is slightly buggy, but it *works* + * [running on android](https://github.com/9001/copyparty#install-on-android) gives you a much better experience +* save the qr-code to a file (txt/svg/png) 202ddeac + +## 🩹 bugfixes + +* #661 fix incorrect `rproxy` hint in the logs 6c76614e +* #649 fix js-crash when tapping in the exactly correct place (thx @hahaslav for debugging!) 0de07d8e +* #628 ftpd: fix banning IPv6 clients 6d76254c + +## 🔧 other changes + +* #296 nixos: support non-flake setups (thx @Sorixelle!) 20ef74cd 32593670 +* config-parser catches and explains a few more common mistakes cc65b1b5 +* docs: + * #490, #199: readme: confirm that combining copyparty and syncthing is safe c51371c7 + * #377 improved authelia docker example (thx @xFuture603!) cd8771fa + * mention the homebrew formulae f9cb2c15 + * #651 versus.md: fix hfs3 comparison (thx @rejetto!) 7a4973fa + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-0817-1556 `v1.19.4` take two (fix cfg vols) diff --git a/scripts/help2html.py b/scripts/help2html.py index 540091ec..4803632b 100755 --- a/scripts/help2html.py +++ b/scripts/help2html.py @@ -7,7 +7,7 @@ import subprocess as sp # to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43: _ = r"""" -echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do +echo; for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do ./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43 """ # click [edit] => [select all] diff --git a/scripts/help2txt.sh b/scripts/help2txt.sh index f5a12565..a046bda3 100755 --- a/scripts/help2txt.sh +++ b/scripts/help2txt.sh @@ -23,7 +23,7 @@ exit 0 # first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it: -for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do +for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do ./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done # then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c diff --git a/scripts/prep.sh b/scripts/prep.sh index d522371e..4e9ae313 100755 --- a/scripts/prep.sh +++ b/scripts/prep.sh @@ -40,7 +40,7 @@ update_mpr_pkgbuild() { update_nixos_pin() { ( cd $self/../contrib/package/nix/copyparty; - ./update.py $self/../dist/copyparty-sfx.py ) + ./update.py $self/../dist/copyparty-$ver.tar.gz ) } update_arch_pkgbuild From 4c3792de07f9cedef7d7c4730a9bb6df8bee972d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 27 Aug 2025 21:03:02 +0000 Subject: [PATCH 110/154] update pkgs to 1.19.6 --- 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 d115019b..187ea058 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.19.5" +pkgver="1.19.6" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("366e0e73d0e322e1dba9315baa328a6cd488f272cb431b9187da326361c2d7da") +sha256sums=("54716d6f6605a9db3d34c93614687a6962d382473a26a9a4a5e0ac833458fd49") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 1a64f4c4..ce09a13c 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.5 +pkgver=1.19.6 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("366e0e73d0e322e1dba9315baa328a6cd488f272cb431b9187da326361c2d7da") +sha256sums=("54716d6f6605a9db3d34c93614687a6962d382473a26a9a4a5e0ac833458fd49") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 80347478..6d1789b5 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.19.5/copyparty-1.19.5.tar.gz", - "version": "1.19.5", - "hash": "sha256-Nm4Oc9DjIuHbqTFbqjKKbNSI8nLLQxuRh9oyY2HC19o=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.6/copyparty-1.19.6.tar.gz", + "version": "1.19.6", + "hash": "sha256-VHFtb2YFqds9NMk2FGh6aWLTgkc6JqmkpeCsgzRY/Uk=" } \ No newline at end of file From 0d96786e68fe0d5b79af5bdbf373dc28b90bf978 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 28 Aug 2025 18:15:37 +0200 Subject: [PATCH 111/154] fix using empty dir as state storage; also supports 4111 (d--x--x--x) XDG_CONFIG_HOME --- copyparty/__main__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 944fa665..50386867 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -204,14 +204,14 @@ def init_E(EE: EnvParams) -> None: continue p = os.path.normpath(p) - if os.path.isdir(p) and os.listdir(p): - mkdir = False - else: - mkdir = True + mkdir = not os.path.isdir(p) + if mkdir: os.mkdir(p) p = os.path.join(p, "copyparty") - if not os.path.isdir(p): + try: + os.listdir(p) + except: os.mkdir(p) if npath > 1: From 914686ec7cac86afe388e6747985d8150afaf899 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 28 Aug 2025 18:46:11 +0200 Subject: [PATCH 112/154] fix navigation by holding I/K --- copyparty/web/browser.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index fb6fea90..ba92fd49 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -16365,7 +16365,10 @@ function tree_scrolltoo(q) { } -function tree_neigh(n) { +function tree_neigh(n, ratelimit) { + if (ratelimit && QS('.dumb_loader_thing') && Date.now() - treectl.busied < 5) + return; + var links = QSA(showfile.active() || treectl.texts ? '#docul li>a' : '#treeul li>a+a'); if (!links.length) { treectl.dir_cb = function () { @@ -16635,7 +16638,7 @@ var ahotkeys = function (e) { n = (k == 'KeyI' || k == 'i') ? -1 : (k == 'KeyK' || k == 'k') ? 1 : 0; if (n !== 0) - return tree_neigh(n); + return tree_neigh(n, 1); if (k == 'KeyM' || k == 'm') return tree_up(); @@ -17440,7 +17443,7 @@ var treectl = (function () { xhr.top = top; xhr.dst = dst; xhr.rst = rst; - xhr.ts = Date.now(); + xhr.ts = r.busied = Date.now(); xhr.open('GET', addq(dst, 'tree=' + top + (r.dots ? '&dots' : '') + k), true); xhr.onload = xhr.onerror = r.recvtree; xhr.send(); @@ -17659,7 +17662,7 @@ var treectl = (function () { xhr.back = back xhr.hpush = hpush; xhr.hydrate = hydrate; - xhr.ts = Date.now(); + xhr.ts = r.busied = Date.now(); xhr.open('GET', xhr.top + '?ls' + uq, true); xhr.setRequestHeader('Fnugg', '' + xhr.ts); xhr.onload = xhr.onerror = recvls; From 6f0871173e74aee988c0960c0f13fc574d3bf0b2 Mon Sep 17 00:00:00 2001 From: EmilyxFox <48589793+EmilyxFox@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:07:42 +0200 Subject: [PATCH 113/154] Set org.opencontainers.image.source label correctly in all dockerfiles (#717) --- scripts/docker/Dockerfile.ac | 2 +- scripts/docker/Dockerfile.dj | 2 +- scripts/docker/Dockerfile.im | 2 +- scripts/docker/Dockerfile.iv | 2 +- scripts/docker/Dockerfile.min | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/docker/Dockerfile.ac b/scripts/docker/Dockerfile.ac index c06f99f8..d8f4e03c 100644 --- a/scripts/docker/Dockerfile.ac +++ b/scripts/docker/Dockerfile.ac @@ -1,7 +1,7 @@ FROM alpine:latest WORKDIR /z LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ - org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \ + org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.title="copyparty-ac" \ org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)" diff --git a/scripts/docker/Dockerfile.dj b/scripts/docker/Dockerfile.dj index cdf9f28b..42ba0db0 100644 --- a/scripts/docker/Dockerfile.dj +++ b/scripts/docker/Dockerfile.dj @@ -1,7 +1,7 @@ FROM alpine:latest WORKDIR /z LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ - org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \ + org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.title="copyparty-dj" \ org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection" diff --git a/scripts/docker/Dockerfile.im b/scripts/docker/Dockerfile.im index 94a7ad1a..d406017f 100644 --- a/scripts/docker/Dockerfile.im +++ b/scripts/docker/Dockerfile.im @@ -1,7 +1,7 @@ FROM alpine:latest WORKDIR /z LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ - org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \ + org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.title="copyparty-im" \ org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)" diff --git a/scripts/docker/Dockerfile.iv b/scripts/docker/Dockerfile.iv index a0fe92ed..f0d592b2 100644 --- a/scripts/docker/Dockerfile.iv +++ b/scripts/docker/Dockerfile.iv @@ -1,7 +1,7 @@ FROM alpine:latest WORKDIR /z LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ - org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \ + org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.title="copyparty-iv" \ org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)" diff --git a/scripts/docker/Dockerfile.min b/scripts/docker/Dockerfile.min index 4712a8ad..30478090 100644 --- a/scripts/docker/Dockerfile.min +++ b/scripts/docker/Dockerfile.min @@ -1,7 +1,7 @@ FROM alpine:latest WORKDIR /z LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ - org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \ + org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.title="copyparty-min" \ org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding" From 01cf20a02911ce3880521a371b3dbce149555ca3 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 28 Aug 2025 19:41:42 +0000 Subject: [PATCH 114/154] docker: change $HOME to `/state` --- scripts/docker/Dockerfile.ac | 2 +- scripts/docker/Dockerfile.dj | 2 +- scripts/docker/Dockerfile.im | 2 +- scripts/docker/Dockerfile.iv | 2 +- scripts/docker/Dockerfile.min | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/docker/Dockerfile.ac b/scripts/docker/Dockerfile.ac index d8f4e03c..027e0366 100644 --- a/scripts/docker/Dockerfile.ac +++ b/scripts/docker/Dockerfile.ac @@ -16,6 +16,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./ ADD base ./base RUN ash innvikler.sh ac -WORKDIR /w +WORKDIR /state EXPOSE 3923 ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.dj b/scripts/docker/Dockerfile.dj index 42ba0db0..02b253f9 100644 --- a/scripts/docker/Dockerfile.dj +++ b/scripts/docker/Dockerfile.dj @@ -38,6 +38,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./ ADD base ./base RUN ash innvikler.sh dj -WORKDIR /w +WORKDIR /state EXPOSE 3923 ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.im b/scripts/docker/Dockerfile.im index d406017f..f027df0a 100644 --- a/scripts/docker/Dockerfile.im +++ b/scripts/docker/Dockerfile.im @@ -15,6 +15,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./ ADD base ./base RUN ash innvikler.sh im -WORKDIR /w +WORKDIR /state EXPOSE 3923 ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.iv b/scripts/docker/Dockerfile.iv index f0d592b2..f671cf68 100644 --- a/scripts/docker/Dockerfile.iv +++ b/scripts/docker/Dockerfile.iv @@ -28,6 +28,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./ ADD base ./base RUN ash innvikler.sh iv -WORKDIR /w +WORKDIR /state EXPOSE 3923 ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.min b/scripts/docker/Dockerfile.min index 30478090..464a470c 100644 --- a/scripts/docker/Dockerfile.min +++ b/scripts/docker/Dockerfile.min @@ -13,6 +13,6 @@ RUN apk --no-cache add !pyc \ COPY i/dist/copyparty-sfx.py innvikler.sh ./ RUN ash innvikler.sh min -WORKDIR /w +WORKDIR /state EXPOSE 3923 ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "--no-thumb", "-c", "/z/initcfg"] From d1f75229b57ebff77b6b97620cff11860ffc8788 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 28 Aug 2025 19:45:47 +0000 Subject: [PATCH 115/154] docker: ensure /state writable --- scripts/docker/innvikler.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/docker/innvikler.sh b/scripts/docker/innvikler.sh index 27a3d230..49627f72 100644 --- a/scripts/docker/innvikler.sh +++ b/scripts/docker/innvikler.sh @@ -15,8 +15,8 @@ rm -rf /z/base rm -rf /var/cache/apk/* /root/.cache # initial config; common for all flavors -mkdir /cfg /w -chmod 777 /cfg /w +mkdir /state /cfg /w +chmod 777 /state /cfg /w echo % /cfg > initcfg # unpack sfx and dive in From 14555d5832baa484104efb8136488e9cb804af08 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 28 Aug 2025 20:14:25 +0000 Subject: [PATCH 116/154] add chdir option --- copyparty/__main__.py | 4 ++++ scripts/docker/Dockerfile.ac | 2 +- scripts/docker/Dockerfile.dj | 2 +- scripts/docker/Dockerfile.im | 2 +- scripts/docker/Dockerfile.iv | 2 +- scripts/docker/Dockerfile.min | 2 +- scripts/docker/innvikler.sh | 8 +++++++- tests/util.py | 2 +- 8 files changed, 17 insertions(+), 7 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 50386867..5c6a8979 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1134,6 +1134,7 @@ def add_general(ap, nc, srvname): ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts") ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]") ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password") + ap2.add_argument("--chdir", metavar="PATH", type=u, help="change working-directory to \033[33mPATH\033[0m before mapping volumes") ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m") ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]") @@ -1991,6 +1992,9 @@ def main(argv: Optional[list[str]] = None) -> None: except: sys.exit(1) + if al.chdir: + os.chdir(al.chdir) + if al.ansi: al.no_ansi = False elif not al.no_ansi: diff --git a/scripts/docker/Dockerfile.ac b/scripts/docker/Dockerfile.ac index 027e0366..54d6fef1 100644 --- a/scripts/docker/Dockerfile.ac +++ b/scripts/docker/Dockerfile.ac @@ -18,4 +18,4 @@ RUN ash innvikler.sh ac WORKDIR /state EXPOSE 3923 -ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] +ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.dj b/scripts/docker/Dockerfile.dj index 02b253f9..b377537b 100644 --- a/scripts/docker/Dockerfile.dj +++ b/scripts/docker/Dockerfile.dj @@ -40,4 +40,4 @@ RUN ash innvikler.sh dj WORKDIR /state EXPOSE 3923 -ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] +ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.im b/scripts/docker/Dockerfile.im index f027df0a..f4f8124d 100644 --- a/scripts/docker/Dockerfile.im +++ b/scripts/docker/Dockerfile.im @@ -17,4 +17,4 @@ RUN ash innvikler.sh im WORKDIR /state EXPOSE 3923 -ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] +ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.iv b/scripts/docker/Dockerfile.iv index f671cf68..6155b327 100644 --- a/scripts/docker/Dockerfile.iv +++ b/scripts/docker/Dockerfile.iv @@ -30,4 +30,4 @@ RUN ash innvikler.sh iv WORKDIR /state EXPOSE 3923 -ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"] +ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] diff --git a/scripts/docker/Dockerfile.min b/scripts/docker/Dockerfile.min index 464a470c..8d8f380a 100644 --- a/scripts/docker/Dockerfile.min +++ b/scripts/docker/Dockerfile.min @@ -15,4 +15,4 @@ RUN ash innvikler.sh min WORKDIR /state EXPOSE 3923 -ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "--no-thumb", "-c", "/z/initcfg"] +ENTRYPOINT ["python3", "-m", "copyparty", "--no-thumb", "-c", "/z/initcfg"] diff --git a/scripts/docker/innvikler.sh b/scripts/docker/innvikler.sh index 49627f72..75b3ca3d 100644 --- a/scripts/docker/innvikler.sh +++ b/scripts/docker/innvikler.sh @@ -17,7 +17,13 @@ rm -rf /var/cache/apk/* /root/.cache # initial config; common for all flavors mkdir /state /cfg /w chmod 777 /state /cfg /w -echo % /cfg > initcfg +cat >initcfg <<'EOF' +[global] + chdir: /w + no-crt + +% /cfg +EOF # unpack sfx and dive in python3 copyparty-sfx.py --version diff --git a/tests/util.py b/tests/util.py index a5674808..a0daef34 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 chmod_f chpw_db doctitle df exit favico ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" + ex = "ah_alg bname chdir chmod_f chpw_db doctitle df exit favico ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner" From 26a29797a6c476bffffb8c9cc332d578f6bc1614 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 28 Aug 2025 20:14:50 +0000 Subject: [PATCH 117/154] v1.19.7 --- copyparty/__version__.py | 4 ++-- docs/changelog.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 8994d880..2b7fbbf9 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 6) +VERSION = (1, 19, 7) CODENAME = "usernames" -BUILD_DT = (2025, 8, 27) +BUILD_DT = (2025, 8, 28) 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 30d0fb30..d83fc584 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,36 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-0827-2038 `v1.19.6` auth-precedence + +## 🧪 new features + +* #673 add Portuguese translation (thx anonymous!) 4b8c2215 + * ...and enable the Polish translation (whoops) 8f235be6 +* #689 add option to control authentication priority/precedence 543b7ea9 +* url-parameter `?dl` forces file download instead of displaying in-browser 48d6224e +* #533 more ways to make the QR-code always-visible in the console 2848941e +* #695 option to log invalid xml from clients 28b93d79 +* #552 configurable markdown newline behavior 0491123b + * and tweak the styling of monospace in links 68503444 + +## 🩹 bugfixes + +* #628 FTP-server now accepts connections from IPv6 link-local addresses 978801d0 +* incorrect assumption that all IPv6 link-local addresses start with `fe80` d39c74c1 +* ftp: fix file rename d40f061a +* u2c: couldn't upload files located at the very top of the unix file hierarchy 599e82f2 +* #699 markdown-editor: fix panic if the table-formatter is executed on something that isn't a table 4c042b3c + +## 🔧 other changes + +* #696 a volume can be one single file, not just folders aa1c9213 +* #442 strongly prefer XDG_CONFIG_HOME as config location 35472557 +* #691 album-art collected from audio-files can now become folder thumbnails 0b50fde3 +* allow spaces in more of the comma-separated options d30240b4 +* docs: + * mention config requirements for [syncing folders](https://github.com/9001/copyparty/#folder-sync) with u2c 6cd0a396 59f142cd + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-0821-2319 `v1.19.5` it runs on iOS From 3e90abbf6f24b3bcc4459bda32f0471f51722b65 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 28 Aug 2025 20:24:59 +0000 Subject: [PATCH 118/154] update pkgs to 1.19.7 --- 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 187ea058..49125029 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.19.6" +pkgver="1.19.7" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("54716d6f6605a9db3d34c93614687a6962d382473a26a9a4a5e0ac833458fd49") +sha256sums=("229476fc111de22a46d56c564d5e615bce6ce222321bcb73b336a3ead03d01f9") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index ce09a13c..f491eaee 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.6 +pkgver=1.19.7 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("54716d6f6605a9db3d34c93614687a6962d382473a26a9a4a5e0ac833458fd49") +sha256sums=("229476fc111de22a46d56c564d5e615bce6ce222321bcb73b336a3ead03d01f9") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 6d1789b5..02c7e1ba 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.19.6/copyparty-1.19.6.tar.gz", - "version": "1.19.6", - "hash": "sha256-VHFtb2YFqds9NMk2FGh6aWLTgkc6JqmkpeCsgzRY/Uk=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.7/copyparty-1.19.7.tar.gz", + "version": "1.19.7", + "hash": "sha256-IpR2/BEd4ipG1WxWTV5hW85s4iIyG8tzszaj6tA9Afk=" } \ No newline at end of file From 0469b5a29eda73fe0b8bfa8427181391af5e381d Mon Sep 17 00:00:00 2001 From: Christian Kastner <ckk@kvr.at> Date: Sun, 31 Aug 2025 18:35:14 +0200 Subject: [PATCH 119/154] bubbleparty.sh: process substitution requires bash POSIX shell does not yet support `<(process substitution)`. Signed-off-by: Christian Kastner <ckk@kvr.at> --- bin/bubbleparty.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/bubbleparty.sh b/bin/bubbleparty.sh index f59aaa02..564546ee 100755 --- a/bin/bubbleparty.sh +++ b/bin/bubbleparty.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # usage: ./bubbleparty.sh ./copyparty-sfx.py .... bwrap \ --unshare-all \ From 87539800e80360cf9675cc9910b489919322664a Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 31 Aug 2025 19:06:56 +0200 Subject: [PATCH 120/154] FTPS: add curl example (#734) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a0b71d3a..df138d2f 100644 --- a/README.md +++ b/README.md @@ -1328,6 +1328,8 @@ some recommended FTP / FTPS clients; `wark` = example password: * https://rclone.org/ does FTPS with `tls=false explicit_tls=true` * `lftp -u k,wark -p 3921 127.0.0.1 -e ls` * `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'` +* `curl ftp://127.0.0.1:3921/` (plaintext ftp) +* `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps) config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall: From 09e3018bf9db1edd3236322c707b4bdc6ec21c8c Mon Sep 17 00:00:00 2001 From: xvrqt <git@xvrqt.com> Date: Wed, 3 Sep 2025 01:03:59 -0700 Subject: [PATCH 121/154] nix-module: Add globalExtraConfig option (#751) Added an option, 'services.copyparty.globalExtraConfig', with default value and description to the NixOS Module. The option type is 'str' and the default value is the empty string. This string is appened verbatim to the [global] section of the config. This allows the use of settings which rely on repeated values to be correctly used. For example, the: 'ipu: 255.255.255.1/32=user' key which allows automatic sign in for users of a CIDR subnet. Because attribute sets in Nix must have unique keys, it is not possible to set more than one CIDR subnet/user pair. Signed-off-by: xvrqt <git@xvrqt.com> --- contrib/nixos/modules/copyparty.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contrib/nixos/modules/copyparty.nix b/contrib/nixos/modules/copyparty.nix index 994b2575..1f8242df 100644 --- a/contrib/nixos/modules/copyparty.nix +++ b/contrib/nixos/modules/copyparty.nix @@ -50,6 +50,7 @@ let configStr = '' ${mkSection "global" cfg.settings} + ${cfg.globalExtraConfig} ${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)} ${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)} ''; @@ -131,6 +132,12 @@ 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."; + }; + accounts = mkOption { type = types.attrsOf ( types.submodule ( @@ -373,3 +380,4 @@ in } ); } + From e798a9a53aa5c61f6ea9fca47f6ce991983ef951 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 3 Sep 2025 19:33:48 +0000 Subject: [PATCH 122/154] fix hotkeys on dvorak (closes #298, closes #733); apparently the convention is that hotkeys should follow the letters according to the layout, and not remain in the qwerty position this breaks apart the cluster of media controls (uiojkl), but that's the intended and expected behavior so it should be fine --- contrib/plugins/quickmove.js | 6 +-- copyparty/web/baguettebox.js | 56 +++++++++++++----------- copyparty/web/browser.js | 83 ++++++++++++++++++++---------------- copyparty/web/md2.js | 65 ++++++++++++++++------------ copyparty/web/up2k.js | 12 ++++-- copyparty/web/util.js | 4 +- 6 files changed, 128 insertions(+), 98 deletions(-) diff --git a/contrib/plugins/quickmove.js b/contrib/plugins/quickmove.js index 34b3a0d3..3a0381df 100644 --- a/contrib/plugins/quickmove.js +++ b/contrib/plugins/quickmove.js @@ -101,10 +101,10 @@ function our_hotkey_handler(e) { // bail if either ALT, CTRL, or SHIFT is pressed - if (e.altKey || e.shiftKey || e.isComposing || ctrl(e)) + if (anymod(e)) return main_hotkey_handler(e); // let copyparty handle this keystroke - var key_name = (e.code || e.key) + '', + var keycode = (e.key || e.code) + '', ae = document.activeElement, aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; @@ -114,7 +114,7 @@ if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet)) return main_hotkey_handler(e); // let copyparty handle this keystroke - if (key_name == 'KeyW') { + if (keycode == 'w' || keycode == 'KeyW') { // okay, this one's for us... do the thing action_to_perform(); return ev(e); diff --git a/copyparty/web/baguettebox.js b/copyparty/web/baguettebox.js index 9a298e7b..ef5b81f7 100644 --- a/copyparty/web/baguettebox.js +++ b/copyparty/web/baguettebox.js @@ -278,23 +278,30 @@ window.baguetteBox = (function () { if (modal.busy) return; - if (e.key == '?') - return halp(); - if (anymod(e, true)) return; - var k = (e.code || e.key) + '', v = vid(), pos = -1; + var k = (e.key || e.code) + '', v = vid(); - if (k == "BracketLeft") + if (k.startsWith('Key')) + k = k.slice(3); + else if (k.startsWith('Digit')) + k = k.slice(5); + + var kl = k.toLowerCase(); + + if (k == '?') + return halp(); + + if (k == "[" || k == "BracketLeft") setloop(1); - else if (k == "BracketRight") + else if (k == "]" || k == "BracketRight") setloop(2); - else if (e.shiftKey && k != "KeyR" && k != "R") + else if (e.shiftKey && kl != "r") return; - else if (k == "ArrowLeft" || k == "KeyJ" || k == "Left" || k == "j") + else if (k == "ArrowLeft" || k == "Left" || kl == "j") showPreviousImage(); - else if (k == "ArrowRight" || k == "KeyL" || k == "Right" || k == "l") + else if (k == "ArrowRight" || k == "Right" || kl == "l") showNextImage(); else if (k == "Escape" || k == "Esc") hideOverlay(); @@ -302,33 +309,33 @@ window.baguetteBox = (function () { showFirstImage(e); else if (k == "End") showLastImage(e); - else if (k == "Space" || k == "KeyP" || k == "KeyK") + else if (k == "Space" || k == "Spacebar" || kl == " " || kl == "p" || kl == "k") playpause(); - else if (k == "KeyU" || k == "KeyO") - relseek(k == "KeyU" ? -10 : 10); - else if (k.indexOf('Digit') === 0 && v) - v.currentTime = v.duration * parseInt(k.slice(-1)) * 0.1; - else if (k == "KeyM" && v) { + else if (kl == "u" || kl == "o") + relseek(kl == "u" ? -10 : 10); + else if (v && /^[0-9]$/.test(k)) + v.currentTime = v.duration * parseInt(k) * 0.1; + else if (kl == "m" && v) { v.muted = vmute = !vmute; mp_ctl(); } - else if (k == "KeyV" && v) { + else if (kl == "v" && v) { vloop = !vloop; vnext = vnext && !vloop; setVmode(); } - else if (k == "KeyC" && v) { + else if (kl == "c" && v) { vnext = !vnext; vloop = vloop && !vnext; setVmode(); } - else if (k == "KeyF") + else if (kl == "f") tglfull(); - else if (k == "KeyS" || k == "s") + else if (kl == "s") tglsel(); - else if (k == "KeyR" || k == "r" || k == "R") + else if (kl == "r") rotn(e.shiftKey ? -1 : 1); - else if (k == "KeyY") + else if (kl == "y") dlpic(); } @@ -450,10 +457,11 @@ window.baguetteBox = (function () { if (anymod(e)) return; - var k = e.code + ''; + var k = (e.key || e.code) + ''; - if (k == "Space") - ev(e); + if (k == "Space" || k == "Spacebar" || k == " ") { + return ev(e); + } } var passiveSupp = false; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index ba92fd49..f6cea77d 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -11787,6 +11787,7 @@ var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext), hash0 = location.hash, sloc0 = '' + location, noih = /[?&]v\b/.exec(sloc0), + dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0), abrt_key = "", rtt = null, ldks = [], @@ -14528,7 +14529,7 @@ var fileman = (function () { exm.onkeydown = exh.onkeydown = exd.onkeydown = sh_k.onkeydown = sh_pw.onkeydown = function (e) { - var kc = (e.code || e.key) + ''; + var kc = (e.key || e.code) + ''; if (kc.endsWith('Enter')) sh_apply.click(); }; @@ -14737,7 +14738,7 @@ var fileman = (function () { (function (a) { f[a].inew.onkeydown = function (e) { rn_ok(a, true); - var kc = (e.code || e.key) + ''; + var kc = (e.key || e.code) + ''; if (kc.endsWith('Enter')) return rn_apply(); }; @@ -14835,7 +14836,7 @@ var fileman = (function () { spresets(); ire.onkeydown = ifmt.onkeydown = function (e) { - var k = (e.code || e.key) + ''; + var k = (e.key || e.code) + ''; if (k == 'Escape' || k == 'Esc') return rn_cancel(); @@ -15307,7 +15308,7 @@ var fileman = (function () { (function (a) { var inew = ebi('rn_new_' + a); inew.onkeydown = function (e) { - if (((e.code || e.key) + '').endsWith('Enter')) + if (((e.key || e.code) + '').endsWith('Enter')) return rn_apply(); }; inew.oninput = function (e) { @@ -16493,10 +16494,20 @@ var ahotkeys = function (e) { if (QS('#bbox-overlay.visible') || modal.busy) return; - var k = (e.code || e.key) + '', pos = -1, n, + var k = (e.key || e.code) + '', pos = -1, n, ae = document.activeElement, aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; + if (k.startsWith('Key')) + k = k.slice(3); + else if (k.startsWith('Digit')) + k = k.slice(5); + + var kl = k.toLowerCase(); + + if (dbg_kbd) + console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which); + if (k == 'Escape' || k == 'Esc') { ae && ae.blur(); tt.hide(); @@ -16544,7 +16555,7 @@ var ahotkeys = function (e) { fselfunw(e, ae, d, rem); return ev(e); } - if (k == 'Space' || k == 'Spacebar') { + if (k == 'Space' || k == 'Spacebar' || k == ' ') { clmod(ae, 'sel', 't'); msel.origin_tr(ae); msel.selui(); @@ -16552,7 +16563,7 @@ var ahotkeys = function (e) { } } if (in_ftab || !aet || (ae && ae.closest('#ggrid'))) { - if ((k == 'KeyA' || k == 'a') && ctrl(e)) { + if ((kl == 'a') && ctrl(e)) { var ntot = treectl.lsc.files.length + treectl.lsc.dirs.length, sel = msel.getsel(), all = msel.getall(); @@ -16568,7 +16579,7 @@ var ahotkeys = function (e) { } if (ae && ae.closest('pre')) { - if ((k == 'KeyA' || k == 'a') && ctrl(e)) { + if ((kl == 'a') && ctrl(e)) { var sel = document.getSelection(), ran = document.createRange(); @@ -16585,107 +16596,105 @@ var ahotkeys = function (e) { if (aet && aet != 'a' && aet != 'tr' && aet != 'td' && aet != 'div' && aet != 'pre') return; - if (e.key == '?') + if (k == '?') return hkhelp(); if (!e.shiftKey && ctrl(e)) { var sel = window.getSelection && window.getSelection() || {}; sel = sel && !sel.isCollapsed && sel.direction != 'none'; - if (k == 'KeyX' || k == 'x') + if (kl == 'x') return fileman.cut(e); - if ((k == 'KeyC' || k == 'c') && !sel) + if (kl == 'c' && !sel) return fileman.cpy(e); - if (k == 'KeyV' || k == 'v') + if (kl == 'v') return fileman.d_paste(e); - if (k == 'KeyK' || k == 'k') + if (kl == 'k') return fileman.delete(e); return; } - if (e.shiftKey && k != 'KeyA' && k != 'KeyD' && k != 'A' && k != 'D') + if (e.shiftKey && kl != 'a' && kl != 'd') return; - if (k.indexOf('Digit') === 0) - pos = parseInt(k.slice(-1)) * 0.1; + if (/^[0-9]$/.test(k)) + pos = parseInt(k) * 0.1; if (pos !== -1) return seek_au_mul(pos) || true; - if (k == 'KeyJ' || k == 'j') + if (kl == 'j') return prev_song() || true; - if (k == 'KeyL' || k == 'l') + if (kl == 'l') return next_song() || true; - if (k == 'KeyP' || k == 'p') + if (kl == 'p') return playpause() || true; - n = (k == 'KeyU' || k == 'u') ? -10 : - (k == 'KeyO' || k == 'o') ? 10 : 0; + n = kl == 'u' ? -10 : kl == 'o' ? 10 : 0; if (n !== 0) return seek_au_rel(n) || true; - if (k == 'KeyY') + if (kl == 'y') return msel.getsel().length ? ebi('seldl').click() : showfile.active() ? ebi('dldoc').click() : dl_song(); - n = (k == 'KeyI' || k == 'i') ? -1 : - (k == 'KeyK' || k == 'k') ? 1 : 0; + n = kl == 'i' ? -1 : kl == 'k' ? 1 : 0; if (n !== 0) return tree_neigh(n, 1); - if (k == 'KeyM' || k == 'm') + if (kl == 'm') return tree_up(); - if (k == 'KeyB' || k == 'b') + if (kl == 'b') return treectl.hidden ? treectl.entree() : treectl.detree(); - if (k == 'KeyG' || k == 'g') + if (kl == 'g') return ebi('griden').click(); - if (k == 'KeyT' || k == 't') + if (kl == 't') return ebi('thumbs').click(); - if (k == 'KeyV' || k == 'v') + if (kl == 'v') return ebi('filetree').click(); if (k == 'F2') return fileman.rename(); if (!treectl.hidden && (!e.shiftKey || !thegrid.en)) { - if (k == 'KeyA' || k == 'a') + if (kl == 'a') return QS('#twig').click(); - if (k == 'KeyD' || k == 'd') + if (kl == 'd') return QS('#twobytwo').click(); } if (showfile.active()) { - if (k == 'KeyS' || k == 's') + if (kl == 's') showfile.tglsel(); - if ((k == 'KeyE' || k == 'e') && ebi('editdoc').style.display != 'none') + if (kl == 'e' && ebi('editdoc').style.display != 'none') ebi('editdoc').click(); } if (mp && mp.au && !mp.au.paused) { - if (k == 'KeyS') + if (kl == 's') return sel_song(); } if (thegrid.en) { - if (k == 'KeyS' || k == 's') + if (kl == 's') return ebi('gridsel').click(); - if (k == 'KeyA' || k == 'a') + if (kl == 'a') return QSA('#ghead a[z]')[0].click(); - if (k == 'KeyD' || k == 'd') + if (kl == 'd') return QSA('#ghead a[z]')[1].click(); } }; diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index a8229366..bb93247f 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -1,6 +1,10 @@ "use strict"; +var sloc0 = '' + location, + dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0); + + // server state var server_md = dom_src.value; @@ -936,23 +940,30 @@ var set_lno = (function () { // hotkeys / toolbar (function () { - var keydown = function (ev) { - if (!ev && window.event) { - ev = window.event; + var keydown = function (e) { + if (!e && window.event) { + e = window.event; if (dev_fbw == 1) { toast.warn(10, 'hello from fallback code ;_;\ncheck console trace'); console.error('using window.event'); } } - var kc = ev.code || ev.keyCode || ev.which, + var k = (e.key || e.code) + '', editing = document.activeElement == dom_src; - //console.log(ev.key, ev.code, ev.keyCode, ev.which); - if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) { + if (k.startsWith('Key')) + k = k.slice(3); + + var kl = k.toLowerCase(); + + if (dbg_kbd) + console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which); + + if (ctrl(e) && kl == "s") { save(); return false; } - if (ev.code == "Escape" || kc == 27) { + if (k == "Escape" || k == "Esc") { var d = ebi('helpclose'); if (d) d.click(); @@ -960,46 +971,44 @@ var set_lno = (function () { if (editing) set_lno(); - if (ctrl(ev)) { - if (ev.code == "KeyE") { + if (ctrl(e)) { + if (kl == "e") { dom_nsbs.click(); return false; } if (!editing) return true; - if (ev.code == "KeyH" || kc == 72) { - md_header(ev.shiftKey); + if (kl == "h") { + md_header(e.shiftKey); return false; } - if (ev.code == "KeyZ" || kc == 90) { - if (ev.shiftKey) + if (kl == "z") { + if (e.shiftKey) action_stack.redo(); else action_stack.undo(); return false; } - if (ev.code == "KeyY" || kc == 89) { + if (kl == "y") { action_stack.redo(); return false; } - if (ev.code == "KeyK") { + if (kl == "k") { fmt_table(); return false; } - if (ev.code == "KeyU") { + if (kl == "u") { iter_uni(); return false; } - var up = ev.code == "ArrowUp" || kc == 38; - var dn = ev.code == "ArrowDown" || kc == 40; - if (up || dn) { - md_p_jump(dn); + if (k == "ArrowUp" || k == "ArrowDown") { + md_p_jump(k == "ArrowDown"); return false; } - if (ev.code == "KeyX" || ev.code == "KeyC") { - md_cut(ev.code == "KeyX"); + if (kl == "x" || kl == "c") { + md_cut(kl == "x"); return true; //sic } } @@ -1007,18 +1016,18 @@ var set_lno = (function () { if (!editing) return true; - if (ev.code == "Tab" || kc == 9) { - md_indent(ev.shiftKey); + if (k == "Tab") { + md_indent(e.shiftKey); return false; } - if (ev.code == "Home" || kc == 36) { - md_home(ev.shiftKey); + if (k == "Home") { + md_home(e.shiftKey); return false; } - if (!ev.shiftKey && ((ev.code + '').endsWith("Enter") || kc == 13)) { + if (!e.shiftKey && k.endsWith("Enter")) { return md_newline(); } - if (!ev.shiftKey && kc == 8) { + if (!e.shiftKey && k == "Backspace") { return md_backspace(); } } diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index a7094859..c512ea7a 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -3037,10 +3037,12 @@ function up2k_init(subtle) { if (anymod(e)) return; - if (e.code == 'ArrowUp') + var k = e.key || e.code; + + if (k == 'ArrowUp') bumpthread(1); - if (e.code == 'ArrowDown') + if (k == 'ArrowDown') bumpthread(-1); } @@ -3103,7 +3105,8 @@ function up2k_init(subtle) { ebi('u2szg').addEventListener('blur', read_u2sz); ebi('u2szg').onkeydown = function (e) { if (anymod(e)) return; - var n = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0; + var k = e.key || e.code, + n = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0; if (!n) return; this.value = parseInt(this.value) + n; read_u2sz(); @@ -3180,7 +3183,8 @@ function up2k_init(subtle) { function kd_life(e) { var el = e.target, - d = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0; + k = e.key || e.code, + d = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0; if (anymod(e) || !d) return; diff --git a/copyparty/web/util.js b/copyparty/web/util.js index b3a3c145..7bcc60ad 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -1824,12 +1824,12 @@ var modal = (function () { }; var onkey = function (e) { - var k = (e.code || e.key) + '', + var k = (e.key || e.code) + '', eok = ebi('modal-ok'), eng = ebi('modal-ng'), ae = document.activeElement; - if (k == 'Space' && ae && (ae === eok || ae === eng)) + if ((k == 'Space' || k == 'Spacebar' || k == ' ') && ae && (ae === eok || ae === eng)) k = 'Enter'; if (k.endsWith('Enter')) { From bfcb6eac4171957f681b39cddd1abbb64a177b9b Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 3 Sep 2025 19:37:24 +0000 Subject: [PATCH 123/154] fix chrome reverting video pause toggles pausing a video with spacebar while video is focused would first get handled by the js hotkey, and then chrome would ignore our hint that bubbling should cease and undo it anyways --- copyparty/web/baguettebox.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/copyparty/web/baguettebox.js b/copyparty/web/baguettebox.js index ef5b81f7..931d5195 100644 --- a/copyparty/web/baguettebox.js +++ b/copyparty/web/baguettebox.js @@ -44,6 +44,7 @@ window.baguetteBox = (function () { loopA = null, loopB = null, url_ts = null, + un_pp = 0, resume_mp = false; var onFSC = function (e) { @@ -337,6 +338,9 @@ window.baguetteBox = (function () { rotn(e.shiftKey ? -1 : 1); else if (kl == "y") dlpic(); + else + return; + return ev(e); } function anim() { @@ -460,6 +464,7 @@ window.baguetteBox = (function () { var k = (e.key || e.code) + ''; if (k == "Space" || k == "Spacebar" || k == " ") { + un_pp = Date.now(); return ev(e); } } @@ -786,8 +791,7 @@ window.baguetteBox = (function () { image.setAttribute('playsinline', '1'); // ios ignores poster image.onended = vidEnd; - image.onplay = function () { show_buttons(1); }; - image.onpause = function () { show_buttons(); }; + image.onplay = image.onpause = ppHandler; } image.alt = thumbnailElement ? thumbnailElement.alt || '' : ''; if (options.titleTag && imageCaption) @@ -802,6 +806,15 @@ window.baguetteBox = (function () { callback(); } + function ppHandler() { + var now = Date.now(); + if (now - un_pp < 50) { + un_pp = 0; + return playpause(); // browser undid space hotkey + } + show_buttons(this.paused ? 1 : 0); + } + function showNextImage(e) { ev(e); return show(currentIndex + 1); From f0caf88185294e398fb22abc0808e71af4f4b574 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 3 Sep 2025 19:45:19 +0000 Subject: [PATCH 124/154] add konmai quality blame msw for this :p --- copyparty/web/browser.js | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index f6cea77d..2b80f8bc 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -16487,6 +16487,13 @@ function fselfunw(e, ae, d, rem) { } selfun(); } +var konmai = 0, konmak = (function() { + var u = "arrowup", + d = "arrowdown", + l = "arrowleft", + r = "arrowright"; + return [u, u, d, d, l, r, l, r, "b", "a", "enter"]; +})(); var ahotkeys = function (e) { if (e.altKey || e.isComposing) return; @@ -16508,6 +16515,19 @@ var ahotkeys = function (e) { if (dbg_kbd) console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which); + if (konmai < 0) + noop(); + else if (konmak[konmai] != kl) + konmai = konmai && kl == konmak[0] ? (konmai<3?konmai:1):0; + else if (++konmai >= konmak.length) { + konmai = -1; + apply_perms(treectl.lsc); + fileman.render(); + document.documentElement.scrollTop = 0; + toast.inf(9, 'omega clearance granted', null, 'top'); + return ev(e); + } + if (k == 'Escape' || k == 'Esc') { ae && ae.blur(); tt.hide(); @@ -17992,6 +18012,7 @@ var treectl = (function () { return r.reqls(get_evpath(), false, undefined, true); } ls0.unlist = unlist0; + ls0.u2ts = u2ts; var top = get_evpath(); if (r.chk_index_html(top, ls0)) @@ -18227,6 +18248,18 @@ var wfp_debounce = (function () { function apply_perms(res) { perms = res.perms || []; + var axs = [], + aclass = '>', + chk = ['read', 'write', 'move', 'delete', 'get', 'admin']; + + if (konmai < 0) { + acct = 'Ted Faro'; + srvinf = 'FAS Nexus</span> // <span>57.3 EiB free of 127 EiB'; + perms = res.perms = chk; + have_up2k_idx = have_tags_idx = 1; + have_shr = have_mv = have_del = true; + } + var a = QS('#ops a[data-dest="up2k"]'); if (have_up2k_idx) { a.removeAttribute('data-perm'); @@ -18241,10 +18274,6 @@ function apply_perms(res) { a.style.display = ''; tt.att(QS('#ops')); - var axs = [], - aclass = '>', - chk = ['read', 'write', 'move', 'delete', 'get', 'admin']; - for (var a = 0; a < chk.length; a++) if (has(perms, chk[a])) axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1)); From b59b915962dac3d4a2444107621f99bc80a05208 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 3 Sep 2025 19:48:47 +0000 Subject: [PATCH 125/154] ie11 fixes --- copyparty/web/browser.css | 4 ++++ copyparty/web/md.js | 4 +++- copyparty/web/md2.js | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 90cb9be8..407967b9 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1555,11 +1555,13 @@ html { #treepar { z-index: 1; position: fixed; + background: #fff; background: var(--tree-bg); left: -.96em; width: calc(.3em + var(--nav-sz) - var(--sbw)); border-bottom: 1px solid var(--bg-u5); overflow: hidden; + border-right: .5em solid #999\9; } #treepar.off { display: none; @@ -1995,6 +1997,7 @@ html.y #doc .line-highlight { } #seldoc.sel { color: var(--fg2-max); + background: #f0f; background: var(--g-sel-b1); } #pvol, @@ -2014,6 +2017,7 @@ a.btn, user-select: none; } #hkhelp { + background: #fff; background: var(--bg); } #hkhelp table { diff --git a/copyparty/web/md.js b/copyparty/web/md.js index d6064c9e..66be69bb 100644 --- a/copyparty/web/md.js +++ b/copyparty/web/md.js @@ -217,7 +217,7 @@ function convert_markdown(md_text, dest_dom) { catch (ex) { if (IE) { dest_dom.innerHTML = 'IE cannot into markdown ;_;'; - return; + return false; } if (ext) @@ -344,6 +344,8 @@ function convert_markdown(md_text, dest_dom) { } catch (ex) { } }, 1); + + return true; } diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index bb93247f..45403f99 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -1051,7 +1051,9 @@ ebi('help').onclick = function (e) { var dom = ebi('helpbox'); var dtxt = dom.getElementsByTagName('textarea'); if (dtxt.length > 0) { - convert_markdown(dtxt[0].value, dom); + var txt = dtxt[0].value; + if (!convert_markdown(txt, dom)) + dom.innerText = txt.split('## markdown')[0]; dom.innerHTML = '<a href="#" id="helpclose">close</a>' + dom.innerHTML; } From c71128fd7222287b9437486a0e391bff079c3955 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 3 Sep 2025 19:50:54 +0000 Subject: [PATCH 126/154] ignore cppws on plaintext; cppws, if set from https context, cannot be cleared by plaintext this could lead to confusing login/logout behavior --- copyparty/httpcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 171791a7..a379f655 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -567,7 +567,7 @@ class HttpCli(object): return False zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x] cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll} - cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or "" + cookie_pw = cookies.get("cppws" if self.is_https else "cppwd") or "" if "b" in cookies and "b" not in uparam: uparam["b"] = cookies["b"] if len(cookies) > self.args.cookie_nmax: From 230a1462090c3cbdd32192c43fdc81205dfed42d Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 3 Sep 2025 19:57:28 +0000 Subject: [PATCH 127/154] ignore dotfiles in config-folders; closes #727 macos adds garbage files named ._something.conf into config folders, crashing the config parser --- copyparty/authsrv.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index ff73f19a..4adde56e 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1751,7 +1751,11 @@ class AuthSrv(object): files = os.listdir(E.cfg) except: files = [] - hits = [x for x in files if x.lower().endswith(".conf")] + hits = [ + x + for x in files + if x.lower().endswith(".conf") and not x.startswith(".") + ] if hits: t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n" self.log(t % (E.cfg, ehint, ", ".join(hits)), 3) @@ -3501,7 +3505,9 @@ def expand_config_file( if os.path.isdir(fp): names = list(sorted(os.listdir(fp))) - cnames = [x for x in names if x.lower().endswith(".conf")] + cnames = [ + x for x in names if x.lower().endswith(".conf") and not x.startswith(".") + ] if not cnames: t = "warning: tried to read config-files from folder '%s' but it does not contain any " if names: From e6755aa8a1fe85daf81ac5ba462f96c26ff505ba Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Wed, 3 Sep 2025 21:55:07 +0000 Subject: [PATCH 128/154] restrict runtime-state in $TMP; closes #747 the preferred locations (XDG_CONFIG_HOME and ~/.config) are trusted and will behave as before, because they are only writable by the current unix-user but when an emergency fallback location ($TMPDIR or /tmp) is used because none of the preferred locations are writable, then this will now force-disable sessions-db, idp-db, chpw, and shares this security safeguard can be overridden with --unsafe-state will now also create the config folder with chmod 700 (rwx------) --- README.md | 1 + copyparty/__init__.py | 1 + copyparty/__main__.py | 31 +++++++++++++++++++++---------- copyparty/svchub.py | 18 ++++++++++++++++++ 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index df138d2f..f091d122 100644 --- a/README.md +++ b/README.md @@ -2318,6 +2318,7 @@ buggy feature? rip it out by setting any of the following environment variables | `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) | | `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy | | `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider | +| `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations | example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py` diff --git a/copyparty/__init__.py b/copyparty/__init__.py index d1630ba0..a21e520e 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -112,6 +112,7 @@ class EnvParams(object): self.t0 = time.time() self.mod = "" self.cfg = "" + self.scfg = True E = EnvParams() diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 5c6a8979..fedef803 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -36,6 +36,7 @@ from .__init__ import ( ) from .__version__ import CODENAME, S_BUILD_DT, S_VERSION from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt +from .bos import bos from .cfg import flagcats, onedash from .svchub import SvcHub from .util import ( @@ -186,7 +187,7 @@ def init_E(EE: EnvParams) -> None: E = EE # pylint: disable=redefined-outer-name - def get_unixdir() -> str: + def get_unixdir() -> tuple[str, bool]: paths: list[tuple[Callable[..., Any], str]] = [ (os.environ.get, "XDG_CONFIG_HOME"), (os.path.expanduser, "~/.config"), @@ -197,6 +198,8 @@ def init_E(EE: EnvParams) -> None: ] errs = [] for npath, (pf, pa) in enumerate(paths): + priv = npath < 2 # private/trusted location + ram = npath > 1 # "nonvolatile"; not semantically same as `not priv` p = "" try: p = pf(pa) @@ -206,15 +209,21 @@ def init_E(EE: EnvParams) -> None: p = os.path.normpath(p) mkdir = not os.path.isdir(p) if mkdir: - os.mkdir(p) + os.mkdir(p, 0o700) p = os.path.join(p, "copyparty") + if not priv and os.path.isdir(p): + uid = os.geteuid() + if os.stat(p).st_uid != uid: + p += ".%s" % (uid,) + if os.path.isdir(p) and os.stat(p).st_uid != uid: + raise Exception("filesystem has broken unix permissions") try: os.listdir(p) except: - os.mkdir(p) + os.mkdir(p, 0o700) - if npath > 1: + if ram: t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" errs.append(t % (pa, p)) elif mkdir: @@ -226,13 +235,14 @@ def init_E(EE: EnvParams) -> None: if errs: warn(". ".join(errs)) - return p # type: ignore + return p, priv except Exception as ex: - if p and npath < 2: + if p: t = "Unable to store config in %s [%s] due to %r" errs.append(t % (pa, p, ex)) - raise Exception("could not find a writable path for config") + t = "could not find a writable path for runtime state:\n> %s" + raise Exception(t % ("\n> ".join(errs))) E.mod = os.path.dirname(os.path.realpath(__file__)) if E.mod.endswith("__init__"): @@ -247,7 +257,7 @@ def init_E(EE: EnvParams) -> None: p = os.path.abspath(os.path.realpath(p)) p = os.path.join(p, "copyparty") if not os.path.isdir(p): - os.mkdir(p) + os.mkdir(p, 0o700) os.listdir(p) except: p = "" @@ -260,11 +270,11 @@ def init_E(EE: EnvParams) -> None: elif sys.platform == "darwin": E.cfg = os.path.expanduser("~/Library/Preferences/copyparty") else: - E.cfg = get_unixdir() + E.cfg, E.scfg = get_unixdir() E.cfg = E.cfg.replace("\\", "/") try: - os.makedirs(E.cfg) + bos.makedirs(E.cfg, bos.MKD_700) except: if not os.path.isdir(E.cfg): raise @@ -1453,6 +1463,7 @@ def add_yolo(ap): ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)") + ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets") def add_optouts(ap): diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 50a3e2d4..eca7f8e0 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -976,6 +976,24 @@ class SvcHub(object): t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n" self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1) + if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"): + t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1." + if not al.no_ses: + al.no_ses = True + t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!" + self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1) + if al.idp_store == 1: + al.idp_store = 0 + self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3) + if al.idp_store: + t2 = "ERROR: Cannot enable --idp-store %s" % (t,) + self.log("root", t2, 1) + raise Exception(t2) + if al.shr: + t2 = "ERROR: Cannot enable shares %s" % (t,) + self.log("root", t2, 1) + raise Exception(t2) + def _process_config(self) -> bool: al = self.args From eeb7738b5355549d733e3589a1c31d727949cde7 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 4 Sep 2025 23:31:05 +0000 Subject: [PATCH 129/154] clamp utime to filesystem limits (#539) --- copyparty/bos/bos.py | 44 +++++++++++++++++++++++++++++++++++++++++--- copyparty/ftpd.py | 8 ++------ copyparty/httpcli.py | 7 +------ copyparty/smbd.py | 2 +- copyparty/up2k.py | 19 +++++++++---------- 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/copyparty/bos/bos.py b/copyparty/bos/bos.py index 6c876e04..408244b9 100644 --- a/copyparty/bos/bos.py +++ b/copyparty/bos/bos.py @@ -2,18 +2,22 @@ from __future__ import print_function, unicode_literals import os +import time from ..util import SYMTIME, fsdec, fsenc from . import path as path if True: # pylint: disable=using-constant-test - from typing import Any, Optional + from typing import Any, Optional, Union + + from ..util import NamedLogger MKD_755 = {"chmod_d": 0o755} MKD_700 = {"chmod_d": 0o700} +UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646)) -_ = (path, MKD_755, MKD_700) -__all__ = ["path", "MKD_755", "MKD_700"] +_ = (path, MKD_755, MKD_700, UTIME_CLAMPS) +__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"] # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" @@ -99,6 +103,40 @@ def utime( return os.utime(fsenc(p), times) +def utime_c( + log: Union["NamedLogger", Any], p: str, ts: int, follow_symlinks: bool = True, throw: bool = False +) -> Optional[int]: + clamp = 0 + ov = ts + bp = fsenc(p) + now = int(time.time()) + while True: + try: + if SYMTIME: + os.utime(bp, (now, ts), follow_symlinks=follow_symlinks) + else: + os.utime(bp, (now, ts)) + if clamp: + t = "filesystem rejected utime(%r); clamped %s to %s" + log(t % (p, ov, ts)) + return ts + except Exception as ex: + pv = ts + while clamp < len(UTIME_CLAMPS): + fun, cv = UTIME_CLAMPS[clamp] + ts = fun(ts, cv) + clamp += 1 + if ts != pv: + break + if clamp >= len(UTIME_CLAMPS): + if throw: + raise + else: + t = "could not utime(%r) to %s; %s, %r" + log(t % (p, ov, ex, ex)) + return None + + if hasattr(os, "lstat"): def lstat(p: str) -> os.stat_result: diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index b0eb8736..ed18a0e7 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -409,12 +409,8 @@ class FtpFs(AbstractedFS): return st def utime(self, path: str, timeval: float) -> None: - try: - ap = self.rv2a(path, w=True)[0] - return bos.utime(ap, (int(time.time()), int(timeval))) - except Exception as ex: - logging.error("ftp.utime: %s, %r", ex, ex) - raise + ap = self.rv2a(path, w=True)[0] + bos.utime_c(logging.warning, ap, int(timeval), False) def lstat(self, path: str) -> os.stat_result: ap = self.rv2a(path)[0] diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index a379f655..072c5962 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -2329,12 +2329,7 @@ class HttpCli(object): at = mt = time.time() - lifetime cli_mt = self.headers.get("x-oc-mtime") if cli_mt: - try: - mt = int(cli_mt) - times = (int(time.time()), mt) - bos.utime(path, times, False) - except: - pass + bos.utime_c(self.log, path, int(cli_mt), False) if nameless and "magic" in vfs.flags: try: diff --git a/copyparty/smbd.py b/copyparty/smbd.py index 7caec6cc..3fbc1130 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -373,7 +373,7 @@ class SMB(object): t = "blocked utime (no-write-acc %s): /%s @%s" yeet(t % (vfs.axs.uwrite, vpath, uname)) - return bos.utime(ap, times) + bos.utime_c(info, ap, int(times[1]), False) def _p_exists(self, vpath: str) -> bool: # ap = "?" diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 1fc52333..1486ac17 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -3435,10 +3435,9 @@ class Up2k(object): cur.connection.commit() ap = djoin(job["ptop"], job["prel"], job["name"]) - times = (int(time.time()), int(cj["lmod"])) - bos.utime(ap, times, False) + mt = bos.utime_c(self.log, ap, int(cj["lmod"]), False, True) - self.log("touched %r from %d to %d" % (ap, job["lmod"], cj["lmod"])) + self.log("touched %r from %d to %d" % (ap, job["lmod"], mt)) except Exception as ex: self.log("umod failed, %r" % (ex,), 3) @@ -3593,8 +3592,7 @@ class Up2k(object): shutil.copy2(fsenc(csrc), fsenc(dst)) if lmod and (not linked or SYMTIME): - times = (int(time.time()), int(lmod)) - bos.utime(dst, times, False) + bos.utime_c(self.log, dst, int(lmod), False) def handle_chunks( self, ptop: str, wark: str, chashes: list[str] @@ -3767,10 +3765,8 @@ class Up2k(object): times = (int(time.time()), int(job["lmod"])) t = "no more chunks, setting times %s (%d) on %r" self.log(t % (times, bos.path.getsize(dst), dst)) - try: - bos.utime(dst, times) - except: - self.log("failed to utime (%r, %s)" % (dst, times)) + bos.utime_c(self.log, dst, times[1], False) + # the above logmsg (and associated logic) is retained due to unforget.py zs = "prel name lmod size ptop vtop wark dwrk host user addr" z2 = [job[x] for x in zs.split()] @@ -4919,7 +4915,10 @@ class Up2k(object): mt = bos.path.getmtime(slabs, False) flags = self.flags.get(ptop) or {} atomic_move(self.log, sabs, slabs, flags) - bos.utime(slabs, (int(time.time()), int(mt)), False) + try: + bos.utime(slabs, (int(time.time()), int(mt)), False) + except: + self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3) self._symlink(slabs, sabs, flags, False, is_mv=True) full[slabs] = (ptop, rem) sabs = slabs From bd6d1f961de8df47f4d5418f4cf8dd584a98b495 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Thu, 4 Sep 2025 23:48:22 +0000 Subject: [PATCH 130/154] konmai intensifies thx SG --- copyparty/web/browser.css | 3 +++ copyparty/web/browser.js | 9 +++++++-- copyparty/web/up2k.js | 6 ++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 407967b9..a40c72e2 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -882,6 +882,9 @@ html.y #path a:hover { #flogout { display: inline; } +html.dz #flogout { + margin-left: 1em; +} #goh+span { color: var(--bg-u5); padding-left: .5em; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 2b80f8bc..189f0a87 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -16521,10 +16521,15 @@ var ahotkeys = function (e) { konmai = konmai && kl == konmak[0] ? (konmai<3?konmai:1):0; else if (++konmai >= konmak.length) { konmai = -1; - apply_perms(treectl.lsc); - fileman.render(); document.documentElement.scrollTop = 0; + settheme.go(6); + start_actx(); + sfx_nice(); toast.inf(9, 'omega clearance granted', null, 'top'); + setTimeout(function() { + apply_perms(treectl.lsc); + fileman.render(); + }, 573); return ev(e); } diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index c512ea7a..a52dd172 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -732,7 +732,7 @@ function Donut(uc, st) { tstrober = setInterval(strobe, 300); if (uc.upsfx && actx && actx.state != 'suspended') - sfx(); + sfx_nice(); // firefox may forget that filedrops are user-gestures so it can skip this: if (uc.upnag && Notification && Notification.permission == 'granted') @@ -745,8 +745,10 @@ function Donut(uc, st) { if (!txt) clearInterval(tstrober); } +} - function sfx() { +function sfx_nice() { + if (true) { var osc = actx.createOscillator(), gain = actx.createGain(), gg = gain.gain, From 7a4ee4dbc87ad31a09f0846f65e5c3f38522d69f Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 16:20:00 +0000 Subject: [PATCH 131/154] apply ipr during login too (#397) --- copyparty/httpcli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 072c5962..8eaed03d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3064,6 +3064,11 @@ class HttpCli(object): uname = self.asrv.iacct.get(hpwd) if uname: pwd = self.asrv.ases.get(uname) or pwd + if uname and self.conn.hsrv.ipr: + znm = self.conn.hsrv.ipr.get(uname) + if znm and not znm.map(self.ip): + self.log("username [%s] rejected by --ipr" % (self.uname,), 3) + uname = "" if uname: msg = "hi " + uname dur = int(60 * 60 * self.args.logout) From c2be664e961e17802d7fad29b8ded06cb843d582 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 18:12:53 +0000 Subject: [PATCH 132/154] cleanup jinja whitespace --- copyparty/web/browser.html | 2 +- copyparty/web/idp.html | 10 +++--- copyparty/web/md.html | 2 +- copyparty/web/mde.html | 2 +- copyparty/web/msg.html | 2 +- copyparty/web/rups.html | 2 +- copyparty/web/shares.html | 10 +++--- copyparty/web/splash.html | 74 +++++++++++++++++++------------------- copyparty/web/svcs.html | 50 +++++++++++++------------- 9 files changed, 77 insertions(+), 77 deletions(-) diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index ebf41f8e..3b5c89ba 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -9,7 +9,7 @@ <meta name="theme-color" content="#{{ tcolor }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}"> -{{ html_head }} +{{- html_head }} {%- if css %} <link rel="stylesheet" media="screen" href="{{ css }}_={{ ts }}"> {%- endif %} diff --git a/copyparty/web/idp.html b/copyparty/web/idp.html index b67dd2f1..8a58f6be 100644 --- a/copyparty/web/idp.html +++ b/copyparty/web/idp.html @@ -10,7 +10,7 @@ <meta name="theme-color" content="#{{ tcolor }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}"> -{{ html_head }} +{{- html_head }} </head> <body> @@ -23,17 +23,17 @@ <th>user</th> <th>groups</th> </tr></thead><tbody> - {% for un, gn in rows %} + {%- for un, gn in rows %} <tr> <td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td> <td>{{ un|e }}</td> <td>{{ gn|e }}</td> </tr> - {% endfor %} + {%- endfor %} </tbody></table> - {% if not rows %} + {%- if not rows %} (there are no IdP users in the cache) - {% endif %} + {%- endif %} </div> <a href="#" id="repl">π</a> <script> diff --git a/copyparty/web/md.html b/copyparty/web/md.html index 1dfed659..429a1eff 100644 --- a/copyparty/web/md.html +++ b/copyparty/web/md.html @@ -9,7 +9,7 @@ {%- if edit %} <link rel="stylesheet" href="{{ r }}/.cpr/md2.css?_={{ ts }}"> {%- endif %} -{{ html_head }} +{{- html_head }} </head> <body> <div id="mn"></div> diff --git a/copyparty/web/mde.html b/copyparty/web/mde.html index f6d0940f..ac3377c6 100644 --- a/copyparty/web/mde.html +++ b/copyparty/web/mde.html @@ -8,7 +8,7 @@ <link rel="stylesheet" href="{{ r }}/.cpr/mde.css?_={{ ts }}"> <link rel="stylesheet" href="{{ r }}/.cpr/deps/mini-fa.css?_={{ ts }}"> <link rel="stylesheet" href="{{ r }}/.cpr/deps/easymde.css?_={{ ts }}"> -{{ html_head }} +{{- html_head }} </head> <body> <div id="mw"> diff --git a/copyparty/web/msg.html b/copyparty/web/msg.html index 4ccac31f..0b966d11 100644 --- a/copyparty/web/msg.html +++ b/copyparty/web/msg.html @@ -8,7 +8,7 @@ <meta name="viewport" content="width=device-width, initial-scale=0.8"> <meta name="theme-color" content="#{{ tcolor }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/msg.css?_={{ ts }}"> -{{ html_head }} +{{- html_head }} </head> <body> diff --git a/copyparty/web/rups.html b/copyparty/web/rups.html index 2cc0443a..fe344c12 100644 --- a/copyparty/web/rups.html +++ b/copyparty/web/rups.html @@ -10,7 +10,7 @@ <meta name="theme-color" content="#{{ tcolor }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/rups.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}"> -{{ html_head }} +{{- html_head }} </head> <body> diff --git a/copyparty/web/shares.html b/copyparty/web/shares.html index 381fe853..de128ab1 100644 --- a/copyparty/web/shares.html +++ b/copyparty/web/shares.html @@ -10,7 +10,7 @@ <meta name="theme-color" content="#{{ tcolor }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}"> -{{ html_head }} +{{- html_head }} </head> <body> @@ -36,7 +36,7 @@ <th>hrs</th> <th>add time</th> </tr></thead><tbody> - {% for k, pw, vp, pr, st, un, t0, t1 in rows %} + {%- for k, pw, vp, pr, st, un, t0, t1 in rows %} <tr> <td> <a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a> @@ -54,11 +54,11 @@ <td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td> <td></td> </tr> - {% endfor %} + {%- endfor %} </tbody></table> - {% if not rows %} + {%- if not rows %} (you don't have any active shares btw) - {% endif %} + {%- endif %} </div> <a href="#" id="repl">π</a> <script> diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 0a886ad4..61dd973d 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -9,7 +9,7 @@ <meta name="theme-color" content="#{{ tcolor }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}"> -{{ html_head }} +{{- html_head }} </head> <body> @@ -18,12 +18,12 @@ <a id="a" href="{{ r }}/?h{{ re }}" class="af">refresh</a> <a id="v" href="{{ r }}/?hc" class="af">connect</a> - {%- if this.uname == '*' %} - <p id="b">howdy stranger   <small>(you're not logged in)</small></p> - {%- else %} - <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> - <p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p> - {%- endif %} + {%- if this.uname == '*' %} + <p id="b">howdy stranger   <small>(you're not logged in)</small></p> + {%- else %} + <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> + <p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p> + {%- endif %} {%- endif %} {%- if msg %} @@ -37,9 +37,9 @@ <table class="vols"> <thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead> <tbody> - {% for u in ups %} + {%- for u in ups %} <tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr> - {% endfor %} + {%- endfor %} </tbody> </table> {%- endif %} @@ -49,9 +49,9 @@ <table class="vols"> <thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead> <tbody> - {% for u in dls %} + {%- for u in dls %} <tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr> - {% endfor %} + {%- endfor %} </tbody> </table> {%- endif %} @@ -70,11 +70,11 @@ <table class="vols"> <thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead> <tbody> - {% for mp in avol %} + {%- for mp in avol %} {%- if mp in vstate and vstate[mp] %} <tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr> {%- endif %} - {% endfor %} + {%- endfor %} </tbody> </table> </td></tr></table> @@ -87,18 +87,18 @@ {%- if rvol %} <h1 id="f">you can browse:</h1> <ul> - {% for mp in rvol %} + {%- for mp in rvol %} <li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li> - {% endfor %} + {%- endfor %} </ul> {%- endif %} {%- if wvol %} <h1 id="g">you can upload to:</h1> <ul> - {% for mp in wvol %} + {%- for mp in wvol %} <li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li> - {% endfor %} + {%- endfor %} </ul> {%- endif %} @@ -110,9 +110,9 @@ <input type="password" id="lp" name="cppwd" placeholder=" password" /> <input type="hidden" name="uhash" id="uhash" value="x" /> <input type="submit" id="ls" value="Unlock" /> - {% if ahttps %} + {%- if ahttps %} <a id="w" href="{{ ahttps }}">switch to https</a> - {% endif %} + {%- endif %} </form> </div> {%- else %} @@ -120,20 +120,20 @@ <div> <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}"> <input type="hidden" id="la" name="act" value="login" /> - {% if this.args.usernames %} + {%- if this.args.usernames %} <input type="text" id="lu" name="uname" placeholder=" username" size="12" /> <input type="password" id="lp" name="cppwd" placeholder=" password" size="12" /> - {% else %} + {%- else %} <input type="password" id="lp" name="cppwd" placeholder=" password" /> - {% endif %} + {%- endif %} <input type="hidden" name="uhash" id="uhash" value="x" /> <input type="submit" id="ls" value="login" /> - {% if chpw %} + {%- if chpw %} <a id="x" href="#">change password</a> - {% endif %} - {% if ahttps %} + {%- endif %} + {%- if ahttps %} <a id="w" href="{{ ahttps }}">switch to https</a> - {% endif %} + {%- endif %} </form> </div> {%- endif %} @@ -142,29 +142,29 @@ <ul> {%- if this.uname in this.args.idp_adm_set %} <li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li> - {% endif %} + {%- endif %} {%- if this.uname != '*' and this.args.shr %} <li><a id="y" href="{{ r }}/?shares">edit shares</a></li> - {% endif %} + {%- endif %} - {% if k304 or k304vis %} - {% if k304 %} + {%- if k304 or k304vis %} + {%- if k304 %} <li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled) {%- else %} <li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled) - {% endif %} + {%- endif %} <blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li> - {% endif %} + {%- endif %} - {% if no304 or no304vis %} - {% if no304 %} + {%- if no304 or no304vis %} + {%- if no304 %} <li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled) {%- else %} <li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled) - {% endif %} + {%- endif %} <blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li> - {% endif %} + {%- endif %} <li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li> <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li> @@ -174,7 +174,7 @@ <input type="hidden" name="act" value="logout" /> <input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" /> </form></li> - {% endif %} + {%- endif %} </ul> </div> diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index 9d139a7b..64e2e0db 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -10,7 +10,7 @@ <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}"> <style>ul{padding-left:1.3em}li{margin:.4em 0}.txa{float:right;margin:0 0 0 1em}</style> -{{ html_head }} +{{- html_head }} </head> <body> @@ -44,7 +44,7 @@ {% if args.have_idp_hdrs %} <p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p> - {% endif %} + {%- endif %} @@ -58,9 +58,9 @@ rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b> </pre> <ul> - {% if s %} + {%- if s %} <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li> - {% endif %} + {%- endif %} <li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li> </ul> @@ -77,9 +77,9 @@ rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b> </pre> <ul> - {% if s %} + {%- if s %} <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li> - {% endif %} + {%- endif %} <li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li> <li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li> </ul> @@ -114,11 +114,11 @@ http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }} </pre> - {% if s %} + {%- if s %} <p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p> - {% endif %} + {%- endif %} </div> - {% endif %} + {%- endif %} @@ -127,24 +127,24 @@ <div class="os win"> <p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p> - {% if args.ftp %} + {%- if args.ftp %} <p>connect with plaintext FTP:</p> <pre> rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b> </pre> - {% endif %} - {% if args.ftps %} + {%- endif %} + {%- if args.ftps %} <p>connect with TLS-encrypted FTPS:</p> <pre> rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b> </pre> - {% endif %} + {%- endif %} <ul> - {% if args.ftps %} + {%- if args.ftps %} <li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li> - {% endif %} + {%- endif %} <li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li> </ul> <p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p> @@ -154,24 +154,24 @@ </div> <div class="os lin"> - {% if args.ftp %} + {%- if args.ftp %} <p>connect with plaintext FTP:</p> <pre> rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b> </pre> - {% endif %} - {% if args.ftps %} + {%- endif %} + {%- if args.ftps %} <p>connect with TLS-encrypted FTPS:</p> <pre> rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b> </pre> - {% endif %} + {%- endif %} <ul> - {% if args.ftps %} + {%- if args.ftps %} <li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li> - {% endif %} + {%- endif %} <li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li> <li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li> </ul> @@ -192,7 +192,7 @@ open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} </pre> </div> - {% endif %} + {%- endif %} @@ -206,9 +206,9 @@ <pre> partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b> </pre> - {% if s %} + {%- if s %} <ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul> - {% endif %} + {%- endif %} <p> you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers) </p> @@ -234,7 +234,7 @@ <pre class="os mac"> open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a' </pre> - {% endif %} + {%- endif %} From 09f22993bef6691dad26d495c0bb91c3db135a5f Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 18:44:30 +0000 Subject: [PATCH 133/154] idp login/logout routes (#761) --- README.md | 4 ++++ copyparty/__main__.py | 10 +++++++++- copyparty/authsrv.py | 3 ++- copyparty/httpcli.py | 4 ++-- copyparty/web/browser.js | 26 ++++++++++++++++++++++++-- copyparty/web/splash.css | 1 + copyparty/web/splash.html | 18 +++++++++++++++++- copyparty/web/splash.js | 8 ++++++-- tests/util.py | 4 +++- 9 files changed, 68 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f091d122..e93e4282 100644 --- a/README.md +++ b/README.md @@ -1942,6 +1942,10 @@ you can disable the built-in password-based login system, and instead replace it * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header + * `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password + +* the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login + * if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index fedef803..86a2fdbe 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -897,6 +897,11 @@ def get_sects(): middleware and not by clients! and, as an extra precaution, send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword) and then \033[36m--idp-h-key finalmasterspark\033[0m to require that + + the login/logout links/buttons can be replaced with links + going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m + will expand \033[36m{dst}\033[0m to the URL of the current page, so + the IdP can redirect the user back to where they were """ ), ], @@ -1303,6 +1308,9 @@ def add_auth(ap): ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP") ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)") + ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page") + ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button") + ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m") ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord") ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app") ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins") @@ -1317,7 +1325,7 @@ def add_auth(ap): ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS) - ap2.add_argument("--ao-has-pw", type=u, default="", help=argparse.SUPPRESS) + ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS) def add_chpw(ap): diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 4adde56e..34a3fe46 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2808,6 +2808,7 @@ class AuthSrv(object): js_htm = { "SPINNER": self.args.spinner, "s_name": self.args.bname, + "idp_login": self.args.idp_login, "have_up2k_idx": "e2d" in vf, "have_acode": not self.args.no_acode, "have_c2flac": self.args.allow_flac, @@ -2879,7 +2880,7 @@ class AuthSrv(object): self.args.ao_idp_before_pw = min(h, hm) < pw self.args.ao_h_before_hm = h < hm self.args.ao_ipu_wins = ipu == 0 - self.args.ao_have_pw = pw < 99 + self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs def load_idp_db(self, quiet=False) -> None: # mutex me diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8eaed03d..8e111386 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -690,7 +690,7 @@ class HttpCli(object): if idp_usr in self.asrv.vfs.aread: self.pw = "" self.uname = idp_usr - if self.args.ao_have_pw: + if self.args.ao_have_pw or self.args.idp_logout: self.html_head += "<script>var is_idp=1</script>\n" else: self.html_head += "<script>var is_idp=2</script>\n" @@ -3051,7 +3051,7 @@ class HttpCli(object): self.asrv.forget_session(self.conn.hsrv.broker, self.uname) self.get_pwd_cookie("x") - dst = self.args.SRS + "?h" + dst = self.args.idp_logout or (self.args.SRS + "?h") h2 = '<a href="' + dst + '">continue</a>' html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst) self.reply(html.encode("utf-8")) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 189f0a87..4106f1b6 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -114,6 +114,7 @@ var Ls = { "gou": 'parent folder">up', "gon": 'next folder">next', "logout": "Logout ", + "login": "Login", "access": " access", "ot_close": "close submenu", "ot_search": "search for files by attributes, path / name, music tags, or any combination of those$N$N<code>foo bar</code> = must contain both «foo» and «bar»,$N<code>foo -bar</code> = must contain «foo» but not «bar»,$N<code>^yana .opus$</code> = start with «yana» and be an «opus» file$N<code>"try unite"</code> = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N<code>2009-12-31</code> or <code>2020-09-12 23:30:00</code>", @@ -745,6 +746,7 @@ var Ls = { "gou": 'naviger ett nivå opp">opp', "gon": 'naviger til mappen etter denne">neste', "logout": "Logg ut ", + "login": "Logg inn", "access": " tilgang", "ot_close": "lukk verktøy", "ot_search": "søk etter filer ved å angi filnavn, mappenavn, tid, størrelse, eller metadata som sangtittel / artist / osv.$N$N<code>foo bar</code> = inneholder både «foo» og «bar»,$N<code>foo -bar</code> = inneholder «foo» men ikke «bar»,$N<code>^yana .opus$</code> = starter med «yana», filtype «opus»$N<code>"try unite"</code> = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", @@ -1375,6 +1377,7 @@ var Ls = { "gou": '顶部">up', "gon": '下一项">next', "logout": " 登出", + "login": "登录", //m "access": " 访问", "ot_close": "关闭子菜单", "ot_search": "按属性、路径/名称、音乐标签或上述内容的任意组合搜索文件$N$N<code>foo bar</code> = 必须包含 «foo» 和 «bar»,$N<code>foo -bar</code> = 包含 «foo» 而不包含 «bar»,$N<code>^yana .opus$</code> = 以 «yama» 为开头的 «opus» 文件$N<code>"try unite"</code> = 正好包含 «try unite»$N$N时间格式为 iso-8601, 比如:$N<code>2009-12-31</code> or <code>2020-09-12 23:30:00</code>", @@ -2009,6 +2012,7 @@ var Ls = { "gou": 'nadřazená složka">nahoru', "gon": 'následující složka">následující', "logout": "Odhlásit ", + "login": "Přihlásit se", //m "access": " přístup", "ot_close": "zavřít podnabídku", "ot_search": "hledat soubory podle atributů, cesty / názvu, hudebních tagů nebo jejich kombinace$N$N<code>foo bar</code> = musí obsahovat jak «foo» tak «bar»,$N<code>foo -bar</code> = musí obsahovat «foo» ale ne «bar»,$N<code>^yana .opus$</code> = začíná na «yana» a je to «opus» soubor$N<code>"try unite"</code> = obsahuje přesně «try unite»$N$Nformát data je iso-8601, jako$N<code>2009-12-31</code> nebo <code>2020-09-12 23:30:00</code>", @@ -2639,6 +2643,7 @@ var Ls = { "gou": 'zum übergeordneter Ordner springen">hoch', "gon": 'zum nächsten Ordner springen">nächst.', "logout": "Abmelden ", + "login": "Anmelden", //m "access": " Zugriff", "ot_close": "Submenu schliessen", "ot_search": "Dateien nach Attributen, Pfad/Name, Musiktags oder beliebiger Kombination suchen$N$N<code>foo bar</code> = muss «foo» und «bar» enthalten,$N<code>foo -bar</code> = muss «foo» aber nicht «bar» enthalten,$N<code>^yana .opus$</code> = beginnt mit «yana» und ist «opus»-Datei$N<code>"try unite"</code> = genau «try unite» enthalten$N$NDatumsformat ist iso-8601, z.B.$N<code>2009-12-31</code> oder <code>2020-09-12 23:30:00</code>", @@ -3269,6 +3274,7 @@ var Ls = { "gou": 'ylempi hakemisto">ylös', "gon": 'seuraava hakemisto">seur', "logout": "Kirjaudu ulos ", + "login": "Kirjaudu sisään", //m "access": " -oikeudet", "ot_close": "sulje alavalikko", "ot_search": "etsi tiedostoja ominaisuuksien, tiedostopolun tai -nimen, musiikkitägien tai näiden yhdistelmän perusteella$N$N<code>foo bar</code> = täytyy sisältää sekä «foo» että «bar»,$N<code>foo -bar</code> = täytyy sisältää «foo» mutta ei «bar»,$N<code>^yana .opus$</code> = alkaa «yana» ja on «opus»-tiedosto$N<code>"try unite"</code> = sisältää täsmälleen «try unite»$N$Npäivämäärän muoto on iso-8601, kuten$N<code>2009-12-31</code> tai <code>2020-09-12 23:30:00</code>", @@ -3899,6 +3905,7 @@ var Ls = { "gou": 'dossier parent">haut', "gon": 'dossier suivant">suivant', "logout": "Déconnexion ", + "login": "Se connecter", //m "access": " accès", "ot_close": "fermer le sous-menu", "ot_search": "chercher des fichiers par leurs attributs, chemin / nom, tag musicaux, ou nimporte quelle combinaison de ces options$N$N<code>foo bar</code> = doit contenir à la fois «foo» et «bar»,$N<code>foo -bar</code> = doit contenir «foo» mais pas «bar»,$N<code>^yana .opus$</code> = commence par «yana» et est un fichier «opus»$N<code>"try unite"</code> = contient exactement «try unite»$N$Nle format de date est iso-8601, comme$N<code>2009-12-31</code> ou <code>2020-09-12 23:30:00</code>", @@ -4529,6 +4536,7 @@ var Ls = { "gou": 'γονικός φάκελος">πάνω', "gon": 'επόμενος φάκελος">επόμενο', "logout": "Αποσύνδεση ", + "login": "Σύνδεση", //m "access": " πρόσβαση", "ot_close": "κλείσιμο υπομενού", "ot_search": "αναζήτηση αρχείων με βάση χαρακτηριστικά, διαδρομή / όνομα, μουσικά tags ή οποιονδήποτε συνδυασμό$N$N<code>foo bar</code> = πρέπει να περιέχει και τα «foo» και «bar»,$N<code>foo -bar</code> = πρέπει να περιέχει το «foo» αλλά όχι το «bar»,$N<code>^yana .opus$</code> = να ξεκινά με «yana» και να είναι αρχείο «opus»$N<code>"try unite"</code> = να περιέχει ακριβώς «try unite»$N$Nη μορφή ημερομηνίας είναι iso-8601, όπως$N<code>2009-12-31</code> ή <code>2020-09-12 23:30:00</code>", @@ -5159,6 +5167,7 @@ var Ls = { "gou": 'cartella genitore">su', "gon": 'prossima cartella">succ', "logout": "Logout ", + "login": "Accedi", //m "access": " accesso", "ot_close": "chiudi sottomenu", "ot_search": "cerca file per attributi, percorso / nome, tag musicali, o qualsiasi combinazione di questi$N$N<code>foo bar</code> = deve contenere sia «foo» che «bar»,$N<code>foo -bar</code> = deve contenere «foo» ma non «bar»,$N<code>^yana .opus$</code> = inizia con «yana» ed è un file «opus»$N<code>"try unite"</code> = contiene esattamente «try unite»$N$Nil formato data è iso-8601, come$N<code>2009-12-31</code> o <code>2020-09-12 23:30:00</code>", @@ -5789,6 +5798,7 @@ var Ls = { "gou": '상위 폴더">위로', "gon": '다음 폴더">다음', "logout": "로그아웃 ", + "login": "로그인", //m "access": " 액세스", "ot_close": "하위 메뉴 닫기", "ot_search": "속성, 경로/이름, 음악 태그 또는 이들의 조합으로 파일을 검색합니다.$N$N<code>foo bar</code> = «foo»와 «bar»를 모두 포함해야 함,$N<code>foo -bar</code> = «foo»는 포함하지만 «bar»는 포함하지 않아야 함,$N<code>^yana .opus$</code> = «yana»로 시작하고 «opus» 파일이어야 함$N<code>"try unite"</code> = 정확히 «try unite»를 포함해야 함$N$N날짜 형식은 ISO-8601입니다. 예:$N<code>2009-12-31</code> 또는 <code>2020-09-12 23:30:00</code>", @@ -6419,6 +6429,7 @@ var Ls = { "gou": 'Bovenligende map">Omhoog', "gon": 'Volgende map">Volgende', "logout": "Uitloggen ", + "login": "Inloggen", //m "access": " Toegang", "ot_close": "Sluit onder-menu", "ot_search": "Zoek voor bestanden bij attributes, pad / naam, muziek tags, of elk andere combinatie tussen$N$N<code>foo bar</code> = moet beide «foo» en «bar» bevatten,$N<code>foo -bar</code> = moet «foo» bevatten maar geen «bar»,$N<code>^yana .opus$</code> = start met «yana» en moet een «opus» bestand zijn$N<code>"try unite"</code> = moet precies «try unite» bevatten$N$Nde datum formaat is iso-8601, zoals$N<code>2009-12-31</code> of <code>2020-09-12 23:30:00</code>", @@ -7050,6 +7061,7 @@ var Ls = { "gou": 'navigér eitt nivå opp">opp', "gon": 'navigér åt mappa etter den her">neste', "logout": "Logg ut ", + "login": "Logg inn", "access": " åtgang", "ot_close": "lukk reiskap", "ot_search": "søk etter filer ved å angje filnamn, mappenamn, tid, storleik, eller metadata som songtittel / artist / osv.$N$N<code>foo bar</code> = inneheld båe «foo» og «bar»,$N<code>foo -bar</code> = innehold «foo» men ikkje «bar»,$N<code>^yana .opus$</code> = startar med «yana», filtype «opus»$N<code>"try unite"</code> = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", @@ -7679,6 +7691,7 @@ var Ls = { "gou": 'nadrzędny folder">w górę', "gon": 'następny folder">następny', "logout": "Wyloguj ", + "login": "Zaloguj się", //m "access": " dostęp", "ot_close": "zamknij pod-menu", "ot_search": "szukaj plików po atrybutach, ścieżce / nazwie, tagach muzyki, bądź dowolnej ich kombinacji$N$N<code>foo bar</code> = musi zawierać «foo» oraz «bar»,$N<code>foo -bar</code> = musi zawierać «foo», lecz nie «bar»,$N<code>^yana .opus$</code> = musi zaczynać się od «yana» i być plikiem «opus»$N<code>"try unite"</code> = zawierać dokładnie «try unite»$N$Nformatem daty jest iso-8601, czyli$N<code>2009-12-31</code> lub <code>2020-09-12 23:30:00</code>", @@ -8307,6 +8320,7 @@ var Ls = { "gou": 'pasta pai">acima', "gon": 'próxima pasta">próximo', "logout": "Sair ", + "login": "Fazer login", "access": " acesso", "ot_close": "fechar submenu", "ot_search": "procurar arquivos por atributos, caminho / nome, tags de música ou qualquer combinação deles$N$N<code>foo bar</code> = deve conter ambos «foo» e «bar»,$N<code>foo -bar</code> = deve conter «foo» mas não «bar»,$N<code>^yana .opus$</code> = começar com «yana» e ser um arquivo «opus»$N<code>"try unite"</code> = conter exatamente «try unite»$N$No formato de data é iso-8601, como$N<code>2009-12-31</code> ou <code>2020-09-12 23:30:00</code>", @@ -8937,6 +8951,7 @@ var Ls = { "gou": 'родительская папка">вверх', "gon": 'следующая папка">след', "logout": "Выйти ", + "login": "Войти", //m "access": " доступ", "ot_close": "закрыть подменю", "ot_search": "искать файлы по атрибутам, пути / имени, музыкальным тегам или любой другой комбинации из следующих конструкций$N$N<code>foo bar</code> = обязано содержать «foo» И «bar»,$N<code>foo -bar</code> = обязано содержать «foo», но не «bar»,$N<code>^yana .opus$</code> = начинается с «yana» и имеет расширение «opus»$N<code>"try unite"</code> = содержит именно «try unite»$N$Nформат времени задаётся по стандарту iso-8601, например$N<code>2009-12-31</code> или <code>2020-09-12 23:30:00</code>", @@ -9567,6 +9582,7 @@ var Ls = { "gou": 'carpeta de nivel superior">subir', "gon": 'siguiente carpeta">siguiente', "logout": "Cerrar sesión ", + "login": "Iniciar sesión", //m "access": " acceso", "ot_close": "cerrar submenú", "ot_search": "buscar archivos por atributos, ruta / nombre, etiquetas de música, o cualquier combinación$N$N<code>foo bar</code> = debe contener «foo» y «bar»,$N<code>foo -bar</code> = debe contener «foo» pero no «bar»,$N<code>^yana .opus$</code> = empieza con «yana» y es un archivo «opus»$N<code>"try unite"</code> = contiene exactamente «try unite»$N$Nel formato de fecha es iso-8601, como$N<code>2009-12-31</code> o <code>2020-09-12 23:30:00</code>", @@ -10196,6 +10212,7 @@ var Ls = { "gou": 'överordnad mapp">upp', "gon": 'nästa mapp">nästa', "logout": "Logga ut ", + "login": "Logga in", //m "access": "-rättighet", "ot_close": "stäng undermeny", "ot_search": "sök efter filer via attribut, sökväg / namn, musiktaggar, eller någon kombination av dessa$N$N<code>foo bar</code> = måste innehålla både «foo» och «bar»,$N<code>foo -bar</code> = måste innehålla «foo» men inte «bar»,$N<code>^yana .opus$</code> = måste börja med «yana» och vara en «opus»-fil$N<code>"try unite"</code> = måste innehålla exakt «try unite»$N$Ndatumformatet är iso-8601, t.ex.$N<code>2009-12-31</code> eller <code>2020-09-12 23:30:00</code>", @@ -10826,6 +10843,7 @@ var Ls = { "gou": 'батьківська папка">вгору', "gon": 'наступна папка">далі', "logout": "Вийти ", + "login": "увійти", //m "access": " доступ", "ot_close": "закрити підменю", "ot_search": "пошук файлів за атрибутами, шляхом / іменем, музичними тегами, або будь-якою комбінацією$N$N<code>foo bar</code> = має містити «foo» і «bar»,$N<code>foo -bar</code> = має містити «foo», але не «bar»,$N<code>^yana .opus$</code> = починатися з «yana» і бути файлом «opus»$N<code>"try unite"</code> = містити точно «try unite»$N$Nформат дати - iso-8601, наприклад$N<code>2009-12-31</code> або <code>2020-09-12 23:30:00</code>", @@ -18289,10 +18307,14 @@ function apply_perms(res) { axs += '-Only'; } + var dst = "?h"; + if (idp_login && acct == "*") + dst = idp_login.replace(/\{dst\}/g, get_evpath()); + ebi('acc_info').innerHTML = '<span id="srv_info2"><span>' + srvinf + '</span></span><span' + aclass + axs + L.access + '</span>' + (acct != '*' ? - '<form id="flogout" method="post" enctype="multipart/form-data"><input type="hidden" name="act" value="logout" /><input id="blogout" type="submit" value="' + (window.is_idp ? '' : L.logout) + acct + '"></form>' : - '<a href="?h">Login</a>'); + '<form id="flogout" method="post" enctype="multipart/form-data"><input type="hidden" name="act" value="logout" /><input id="blogout" type="submit" value="' + L.logout + acct + '"></form>' : + '<a href="' + dst + '">' + L.login + '</a>'); var o = QSA('#ops>a[data-perm]'); for (var a = 0; a < o.length; a++) { diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css index 3b4d0f68..fb5d5075 100644 --- a/copyparty/web/splash.css +++ b/copyparty/web/splash.css @@ -38,6 +38,7 @@ a { td a { margin: 0; } +#wb, #w { color: #fff; background: #940; diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 61dd973d..1b9a89be 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -21,7 +21,11 @@ {%- if this.uname == '*' %} <p id="b">howdy stranger   <small>(you're not logged in)</small></p> {%- else %} + {%- if this.args.idp_logout %} + <a id="c" href="{{ this.args.idp_logout }}" class="logout">logout</a> + {%- else %} <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> + {%- endif %} <p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p> {%- endif %} {%- endif %} @@ -118,6 +122,13 @@ {%- else %} <h1 id="l">login for more:</h1> <div> + {%- if this.args.idp_login %} + <ul><li> + <a href="{{ this.args.idp_login | replace("{dst}",r+"/"+qvpath) }}">{{ this.args.idp_login_t }}</a> + {%- if this.args.ao_have_pw %}or alternatively:{%- endif %} + </li></ul> + {%- endif %} + {%- if this.args.ao_have_pw %} <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}"> <input type="hidden" id="la" name="act" value="login" /> {%- if this.args.usernames %} @@ -135,11 +146,16 @@ <a id="w" href="{{ ahttps }}">switch to https</a> {%- endif %} </form> + {%- endif %} </div> {%- endif %} <h1 id="cc">other stuff:</h1> <ul> + {%- if ahttps %} + <li><a id="wb" href="{{ ahttps }}">switch to https</a></li> + {%- endif %} + {%- if this.uname in this.args.idp_adm_set %} <li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li> {%- endif %} @@ -169,7 +185,7 @@ <li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li> <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li> - {%- if this.uname != '*' %} + {%- if this.uname != '*' and not in_shr %} <li><form method="post" enctype="multipart/form-data"> <input type="hidden" name="act" value="logout" /> <input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" /> diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 150b7dac..4ab25aa5 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -814,6 +814,8 @@ if (window.langmod) var d = Ls[sread("cpp_lang", Object.keys(Ls)) || lang] || Ls.eng || Ls.nor || Ls.chi; +d.wb = d.w; + for (var k in (d || {})) { var f = k.slice(-1), i = k.slice(0, -1), @@ -844,14 +846,16 @@ catch (ex) { } tt.init(); var o = QS('input[name="uname"]') || QS('input[name="cppwd"]'); -if (!MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) +if (o && !MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) o.focus(); o = ebi('u'); if (o && /[0-9]+$/.exec(o.innerHTML)) o.innerHTML = shumantime(o.innerHTML); -ebi('uhash').value = '' + location.hash; +o = ebi('uhash') +if (o) + o.value = '' + location.hash; if (/\&re=/.test('' + location)) ebi('a').className = 'af g'; diff --git a/tests/util.py b/tests/util.py index a0daef34..e4804b38 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 exit favico ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" + ex = "ah_alg bname chdir chmod_f chpw_db doctitle df exit favico ipa html_head idp_login idp_logout lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR" ka.update(**{k: "" for k in ex.split()}) ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner" @@ -289,6 +289,7 @@ class VHttpSrv(object): self.broker = NullBroker(args, asrv) self.prism = None + self.ipr = None self.bans = {} self.tdls = self.dls = {} self.tdli = self.dli = {} @@ -344,6 +345,7 @@ class VHttpConn(object): Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv self.hsrv = Ctor(args, asrv, log) self.ico = Ico(args) + self.ipr = None self.ipa_nm = None self.lf_url = None self.log_func = log From 19a4c453892b8a7e83eb93930357faa1068a9533 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 19:48:38 +0000 Subject: [PATCH 134/154] rbac disk-info and --ver (closes #726); options --du-who and --ver-who specifies who can see the disk-info (disk-usage, disk-free) and server-version based on user permissions --- copyparty/__main__.py | 8 ++++++-- copyparty/authsrv.py | 34 +++++++++++++++++++++++++++++++--- copyparty/cfg.py | 2 ++ copyparty/httpcli.py | 25 ++++++++++++++++++++++--- copyparty/svchub.py | 10 +++++++++- tests/util.py | 4 +++- 6 files changed, 73 insertions(+), 10 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 86a2fdbe..6645e0cb 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1485,7 +1485,7 @@ def add_optouts(ap): ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move") ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>") 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") + 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("--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)") @@ -1763,7 +1763,11 @@ def add_ui(ap, retry): ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents") ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)") ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m") - ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)") + ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all") + ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone") + ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)") + ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS) + ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS) ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 34a3fe46..f68a9f7b 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -1014,7 +1014,10 @@ class AuthSrv(object): yield prev, True def vf0(self): - return {"d2d": True, "tcolor": self.args.tcolor} + return {"d2d": True, "tcolor": self.args.tcolor, "du_iwho": self.args.du_iwho} + + def vf0b(self): + return {"tcolor": self.args.tcolor, "du_iwho": self.args.du_iwho} def idp_checkin( self, broker: Optional["BrokerCli"], uname: str, gname: str @@ -1759,8 +1762,7 @@ class AuthSrv(object): if hits: t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n" self.log(t % (E.cfg, ehint, ", ".join(hits)), 3) - zvf = {"tcolor": self.args.tcolor} - vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf) + vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b()) if not axs.uread: self.badcfg1 = True elif "" not in mount: @@ -2294,6 +2296,8 @@ class AuthSrv(object): vol.lim.uid = vol.flags["uid"] vol.lim.gid = vol.flags["gid"] + vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"]) + if vol.flags.get("og"): self.args.uqe = True @@ -3474,6 +3478,30 @@ class AuthSrv(object): self.log("generated config:\n\n" + "\n".join(ret)) +def n_du_who(s: str) -> int: + if s == "all": + return 9 + if s == "auth": + return 7 + if s == "w": + return 5 + if s == "rw": + return 4 + if s == "a": + return 3 + return 0 + + +def n_ver_who(s: str) -> int: + if s == "all": + return 9 + if s == "auth": + return 6 + if s == "a": + return 3 + return 0 + + def split_cfg_ln(ln: str) -> dict[str, Any]: # "a, b, c: 3" => {a:true, b:true, c:3} ret = {} diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 51c47853..549ffa10 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -84,6 +84,7 @@ def vf_vmap() -> dict[str, str]: "chmod_d", "chmod_f", "dbd", + "du_who", "forget_ip", "hsortn", "html_head", @@ -296,6 +297,7 @@ flagcats = { "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH", "tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)", "nodirsz": "don't show total folder size", + "du_who=all": "show disk-usage info to everyone", "robots": "allows indexing by search engines (default)", "norobots": "kindly asks search engines to leave", "unlistcr": "don't list read-access in controlpanel", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8e111386..dc72bf7d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1632,7 +1632,14 @@ class HttpCli(object): self.log("inaccessible: %r" % ("/" + self.vpath,)) raise Pebkac(401, "authenticate") - if "quota-available-bytes" in props and not self.args.nid: + zi = vn.flags["du_iwho"] if "quota-available-bytes" in props else 0 + if zi and ( + zi == 9 + or (zi == 7 and self.uname != "*") + or (zi == 5 and self.can_write) + or (zi == 4 and self.can_write and self.can_read) + or (zi == 3 and self.can_admin) + ): bfree, btot, _ = get_df(vn.realpath, False) if btot: df = { @@ -5159,6 +5166,11 @@ class HttpCli(object): elif nre: re_btn = "&re=%s" % (nre,) + zi = self.args.ver_iwho + show_ver = zi and ( + zi == 9 or (zi == 6 and self.uname != "*") or (zi == 3 and avol) + ) + html = self.j2s( "splash", this=self, @@ -5181,7 +5193,7 @@ class HttpCli(object): no304=self.no304(), k304vis=self.args.k304 > 0, no304vis=self.args.no304 > 0, - ver=S_VERSION if self.args.ver else "", + ver=S_VERSION if show_ver else "", chpw=self.args.chpw and self.uname != "*", ahttps="" if self.is_https else "https://" + self.host + self.req, ) @@ -6360,7 +6372,14 @@ class HttpCli(object): except: self.log("#wow #whoa") - if not self.args.nid: + zi = vn.flags["du_iwho"] + if zi and ( + zi == 9 + or (zi == 7 and self.uname != "*") + or (zi == 5 and self.can_write) + or (zi == 4 and self.can_write and self.can_read) + or (zi == 3 and self.can_admin) + ): free, total, zs = get_df(abspath, False) if total: h1 = humansize(free or 0) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index eca7f8e0..75a269ad 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -27,7 +27,7 @@ if True: # pylint: disable=using-constant-test from typing import Any, Optional, Union from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode -from .authsrv import BAD_CFG, AuthSrv +from .authsrv import BAD_CFG, AuthSrv, n_du_who, n_ver_who from .bos import bos from .cert import ensure_cert from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN @@ -289,6 +289,14 @@ class SvcHub(object): ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)] args.theme = "{0}{1} {0} {1}".format(ch, bri) + if args.nid: + args.du_who = "no" + args.du_iwho = n_du_who(args.du_who) + + if args.ver and args.ver_who == "no": + args.ver_who = "all" + args.ver_iwho = n_ver_who(args.ver_who) + if args.nih: args.vname = "" args.doctitle = args.doctitle.replace(" @ --name", "") diff --git a/tests/util.py b/tests/util.py index e4804b38..6acc4a42 100644 --- a/tests/util.py +++ b/tests/util.py @@ -158,7 +158,7 @@ class Cfg(Namespace): ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate th_spec_p u2abort u2j u2sz unp_who" ka.update(**{k: 1 for k in ex.split()}) - ex = "ac_convt au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" + ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who ver_iwho zip_who" ka.update(**{k: 9 for k in ex.split()}) 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" @@ -189,6 +189,7 @@ class Cfg(Namespace): cookie_cmax=8192, cookie_nmax=50, dbd="wal", + du_who="all", dk_salt="b" * 16, fk_salt="a" * 16, grp_all="acct", @@ -220,6 +221,7 @@ class Cfg(Namespace): u2sort="s", u2ts="c", unpost=600, + ver_who="all", warksalt="hunter2", **ka ) From 74821a38ad59c982d8fe7ab325260da3939431d3 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 20:38:04 +0000 Subject: [PATCH 135/154] speed --- bin/u2c.py | 10 ++++++---- copyparty/__init__.py | 1 + copyparty/__main__.py | 1 + copyparty/httpcli.py | 5 +++-- copyparty/httpsrv.py | 2 +- copyparty/up2k.py | 8 +++++--- copyparty/util.py | 13 ++++++++----- 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/bin/u2c.py b/bin/u2c.py index f72e0474..aec03918 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.12" -S_BUILD_DT = "2025-08-26" +S_VERSION = "2.13" +S_BUILD_DT = "2025-09-05" """ u2c.py: upload to copyparty @@ -590,9 +590,10 @@ def undns(url): def _scd(err, top): """non-recursive listing of directory contents, along with stat() info""" + top_ = os.path.join(top, b"") with os.scandir(top) as dh: for fh in dh: - abspath = os.path.join(top, fh.name) + abspath = top_ + fh.name try: yield [abspath, fh.stat()] except Exception as ex: @@ -601,8 +602,9 @@ def _scd(err, top): def _lsd(err, top): """non-recursive listing of directory contents, along with stat() info""" + top_ = os.path.join(top, b"") for name in os.listdir(top): - abspath = os.path.join(top, name) + abspath = top_ + name try: yield [abspath, os.stat(abspath)] except Exception as ex: diff --git a/copyparty/__init__.py b/copyparty/__init__.py index a21e520e..f9e7da5b 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -111,6 +111,7 @@ class EnvParams(object): def __init__(self) -> None: self.t0 = time.time() self.mod = "" + self.mod_ = "" self.cfg = "" self.scfg = True diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 6645e0cb..9751fb26 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -247,6 +247,7 @@ def init_E(EE: EnvParams) -> None: E.mod = os.path.dirname(os.path.realpath(__file__)) if E.mod.endswith("__init__"): E.mod = os.path.dirname(E.mod) + E.mod_ = os.path.join(E.mod, "") try: p = os.environ.get("XDG_CONFIG_HOME") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index dc72bf7d..7dc4bc1b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1255,7 +1255,7 @@ class HttpCli(object): res_path = "web/" + self.vpath[5:] if res_path in RES: - ap = os.path.join(self.E.mod, res_path) + ap = self.E.mod_ + res_path if bos.path.exists(ap) or bos.path.exists(ap + ".gz"): return self.tx_file(ap) else: @@ -5408,8 +5408,9 @@ class HttpCli(object): if dk_sz and fsroot: kdirs = [] + fsroot_ = os.path.join(fsroot, "") for dn in dirs: - ap = os.path.join(fsroot, dn) + ap = fsroot_ + dn zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz] kdirs.append(dn + "?k=" + zs) dirs = kdirs diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index 77492d56..8e4e4a4e 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -571,7 +571,7 @@ class HttpSrv(object): v = self.E.t0 try: - with os.scandir(os.path.join(self.E.mod, "web")) as dh: + with os.scandir(self.E.mod_ + "web") as dh: for fh in dh: inf = fh.stat() v = max(v, inf.st_mtime) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 1486ac17..093bbed6 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -414,10 +414,11 @@ class Up2k(object): ret: list[tuple[int, str, int, int, int]] = [] userset = set([(uname or "\n"), "*"]) + e_d = {} n = 1000 try: for ptop, tab2 in self.registry.items(): - cfg = self.flags.get(ptop, {}).get("u2abort", 1) + cfg = self.flags.get(ptop, e_d).get("u2abort", 1) if not cfg: continue addr = (ip or "\n") if cfg in (1, 2) else "" @@ -1138,7 +1139,7 @@ class Up2k(object): ft = "\033[0;32m{}{:.0}" ff = "\033[0;35m{}{:.0}" fv = "\033[0;36m{}:\033[90m{}" - zs = "ext_th_d html_head 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 = "du_iwho ext_th_d html_head 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()) @@ -1493,6 +1494,7 @@ class Up2k(object): files: list[tuple[int, int, str]] = [] fat32 = True cv = vcv = acv = "" + e_d = {} th_cvd = self.args.th_coversd th_cvds = self.args.th_coversd_set @@ -1730,7 +1732,7 @@ class Up2k(object): un = "" # skip upload hooks by not providing vflags - self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at) + self.db_add(db.c, e_d, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at) db.n += 1 db.nf += 1 tfa += 1 diff --git a/copyparty/util.py b/copyparty/util.py index 76b68877..c476a2ea 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -3189,8 +3189,9 @@ def statdir( else: src = "listdir" fun: Any = os.lstat if lstat else os.stat + btop_ = os.path.join(btop, b"") for name in os.listdir(btop): - abspath = os.path.join(btop, name) + abspath = btop_ + name try: yield (fsdec(name), fun(abspath)) except Exception as ex: @@ -3229,7 +3230,9 @@ def rmdirs( stats = statdir(logger, scandir, lstat, top, False) dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)] - dirs = [os.path.join(top, x) for x in dirs] + if dirs: + top_ = os.path.join(top, "") + dirs = [top_ + x for x in dirs] ok = [] ng = [] for d in reversed(dirs): @@ -4205,7 +4208,7 @@ def _pkg_resource_exists(pkg: str, name: str) -> bool: def stat_resource(E: EnvParams, name: str): - path = os.path.join(E.mod, name) + path = E.mod_ + name if os.path.exists(path): return os.stat(fsenc(path)) return None @@ -4252,7 +4255,7 @@ def _has_resource(name: str): def has_resource(E: EnvParams, name: str): - return _has_resource(name) or os.path.exists(os.path.join(E.mod, name)) + return _has_resource(name) or os.path.exists(E.mod_ + name) def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]: @@ -4277,7 +4280,7 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]: stream = codecs.getreader(enc)(stream) return stream - ap = os.path.join(E.mod, name) + ap = E.mod_ + name if PY2: return codecs.open(ap, "r", encoding=enc) # type: ignore From 96b109b0d6e4070984f71727abff0afb65d5b647 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 21:03:30 +0000 Subject: [PATCH 136/154] decrement folder-sz on delete; closes #759, #393 --- copyparty/up2k.py | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 093bbed6..3acf3918 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -1695,7 +1695,7 @@ class Up2k(object): t = "reindex %r => %r mtime(%s/%s) size(%s/%s)" self.log(t % (top, rp, dts, lmod, dsz, sz)) - self.db_rm(db.c, rd, fn, 0) + self.db_rm(db.c, e_d, rd, fn, 0) tfa += 1 db.n += 1 in_db = [] @@ -1792,7 +1792,7 @@ class Up2k(object): rm_files = [x for x in hits if x not in seen_files] n_rm = len(rm_files) for fn in rm_files: - self.db_rm(db.c, rd, fn, 0) + self.db_rm(db.c, e_d, rd, fn, 0) if n_rm: self.log("forgot {} deleted files".format(n_rm)) @@ -3143,7 +3143,7 @@ class Up2k(object): for cur, dp_dir, dp_fn in lost: t = "forgetting desynced db entry: %r" self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn))) - self.db_rm(cur, dp_dir, dp_fn, cj["size"]) + self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"]) if c2 and c2 != cur: c2.connection.commit() @@ -3473,7 +3473,7 @@ class Up2k(object): vrel = vjoin(job["prel"], fname) xlink = bool(vf.get("xlink")) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel) - self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink) + self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink) except Exception as ex: self.log("skipping replace-relink: %r" % (ex,)) finally: @@ -3887,7 +3887,9 @@ class Up2k(object): return True - def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str, sz: int) -> None: + def db_rm( + self, db: "sqlite3.Cursor", vflags: dict[str, Any], rd: str, fn: str, sz: int + ) -> None: sql = "delete from up where rd = ? and fn = ?" try: r = db.execute(sql, (rd, fn)) @@ -3895,9 +3897,22 @@ class Up2k(object): assert self.mem_cur # !rm r = db.execute(sql, s3enc(self.mem_cur, rd, fn)) - if r.rowcount: - self.volsize[db] -= sz - self.volnfiles[db] -= 1 + if not r.rowcount: + return + + self.volsize[db] -= sz + self.volnfiles[db] -= 1 + + if "nodirsz" not in vflags: + try: + q = "update ds set nf=nf-1, sz=sz-? where rd=?" + while True: + db.execute(q, (sz, rd)) + if not rd: + break + rd = rd.rsplit("/", 1)[0] if "/" in rd else "" + except: + pass def db_add( self, @@ -3918,7 +3933,7 @@ class Up2k(object): skip_xau: bool = False, ) -> None: """mutex(main) me""" - self.db_rm(db, rd, fn, sz) + self.db_rm(db, vflags, rd, fn, sz) if not ip: db_ip = "" @@ -4201,7 +4216,7 @@ class Up2k(object): xlink = bool(dbv.flags.get("xlink")) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath) self._forget_file( - ptop, volpath, cur, wark, True, st.st_size, xlink + ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink ) finally: if cur: @@ -4659,7 +4674,14 @@ class Up2k(object): with self.reg_mutex: has_dupes = self._forget_file( - svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize, xlink + svn.realpath, + srem, + svn.flags, + c1, + w, + is_xvol, + fsize_ or fsize, + xlink, ) if not is_xvol: @@ -4804,6 +4826,7 @@ class Up2k(object): self, ptop: str, vrem: str, + vflags: dict[str, Any], cur: Optional["sqlite3.Cursor"], wark: Optional[str], drop_tags: bool, @@ -4828,7 +4851,7 @@ class Up2k(object): q = "delete from mt where w=?" cur.execute(q, (wark[:16],)) - self.db_rm(cur, srd, sfn, sz) + self.db_rm(cur, vflags, srd, sfn, sz) reg = self.registry.get(ptop) if reg: From aaeec11f81740b1068ae952ce2e8795e60d5673a Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 21:43:33 +0000 Subject: [PATCH 137/154] bail from aborted batch operations; closes #748 f.shift() in rename_cb would return null since the queue was dumped --- copyparty/web/browser.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 4106f1b6..33dc350c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -14343,6 +14343,7 @@ function fmt_ren(re, md, fmt) { function fs_abrt() { toast.inf(30, L.fp_abrt); + fileman.sn++; fileman.f.length = 0; var xhr = new XHR(); xhr.open('POST', '/?fs_abrt=' + abrt_key, true); @@ -14361,6 +14362,7 @@ var fileman = (function () { r = {}; r.f = []; + r.sn = 1; r.clip = null; try { r.bus = new BroadcastChannel("fileman_bus"); @@ -14645,6 +14647,7 @@ var fileman = (function () { return toast.err(3, L.fr_eperm); var f = [], + sn = ++r.sn, base = vsplit(sel[0].vp)[0], mkeys; @@ -14940,6 +14943,8 @@ var fileman = (function () { toast.err(9, L.fr_efail + msg); return; } + if (r.sn != sn) + return modal.confirm('WARNING: the rename was aborted'); f.shift().inew.value = '( OK )'; return rn_apply_loop(); @@ -14956,6 +14961,7 @@ var fileman = (function () { r.delete = function (e) { var sel = msel.getsel(), + sn = ++r.sn, vps = []; for (var a = 0; a < sel.length; a++) @@ -14992,6 +14998,9 @@ var fileman = (function () { toast.err(9, L.fd_err + msg); return; } + if (r.sn != sn) + return modal.confirm('WARNING: the delete was aborted'); + if (this.responseText.indexOf('deleted 0 files (and 0') + 1) { toast.err(9, L.fd_none); return deleter('xbd'); @@ -15204,6 +15213,7 @@ var fileman = (function () { '<div><table id="rn_f" class="m">', '<tr><td>' + L.fr_lnew + '</td><td>' + L.fr_lold + '</td></tr>', ], + sn = ++r.sn, ui = false, f = [], indir = [], @@ -15263,6 +15273,9 @@ var fileman = (function () { toast.err(9, (r.ccp ? L.fcp_err : L.fp_err) + msg); return; } + if (r.sn != sn) + return modal.confirm('WARNING: the paste was aborted'); + paster(); } function okgo() { From b049631169960eada21b0bac3bbf99eafe5f5397 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 22:36:16 +0000 Subject: [PATCH 138/154] ftp: CWD is optional (#539) --- copyparty/ftpd.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index ed18a0e7..ab8db97f 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -152,10 +152,6 @@ class FtpFs(AbstractedFS): self.cwd = "/" # pyftpdlib convention of leading slash self.root = "/var/lib/empty" - self.can_read = self.can_write = self.can_move = False - self.can_delete = self.can_get = self.can_upget = False - self.can_admin = self.can_dot = False - self.listdirinfo = self.listdir self.chdir(".") @@ -218,7 +214,7 @@ class FtpFs(AbstractedFS): m: bool = False, d: bool = False, ) -> tuple[str, VFS, str]: - return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d) + return self.v2a(join(self.cwd, vpath), r, w, m, d) def ftp2fs(self, ftppath: str) -> str: # return self.v2a(ftppath) @@ -297,16 +293,6 @@ class FtpFs(AbstractedFS): avfs = vfs self.cwd = nwd - ( - self.can_read, - self.can_write, - self.can_move, - self.can_delete, - self.can_get, - self.can_upget, - self.can_admin, - self.can_dot, - ) = avfs.can_access("", self.h.uname) def mkdir(self, path: str) -> None: ap, vfs, _ = self.rv2a(path, w=True) @@ -329,7 +315,7 @@ class FtpFs(AbstractedFS): vfs_ls = [x[0] for x in vfs_ls1] vfs_ls.extend(vfs_virt.keys()) - if not self.can_dot: + if self.uname not in vfs.axs.udot: vfs_ls = exclude_dotfiles(vfs_ls) vfs_ls.sort() @@ -377,9 +363,6 @@ class FtpFs(AbstractedFS): raise FSE(str(ex)) def rename(self, src: str, dst: str) -> None: - if not self.can_move: - raise FSE("Not allowed for user " + self.h.uname) - if self.args.no_mv: raise FSE("The rename/move feature is disabled in server config") From f7e08ed007c560e74138fbc6b5d3a315302a81d7 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Fri, 5 Sep 2025 23:19:20 +0000 Subject: [PATCH 139/154] defer next-song hotkey while changing folders --- copyparty/web/browser.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 33dc350c..6c1f6184 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -12987,6 +12987,10 @@ function song_skip(n, dirskip) { } function next_song(e) { ev(e); + if (QS('.dumb_loader_thing')) { + treectl.ls_cb = next_song; + return; + } if (mp.order.length) { var dirskip = mpl.traversals; mpl.traversals = 0; @@ -13020,6 +13024,10 @@ function prev_song(e) { if (mp.au && !mp.au.paused && mp.au.currentTime > 3) return seek_au_sec(0); + if (QS('.dumb_loader_thing')) { + treectl.ls_cb = function () { song_skip(-1); }; + return; + } return song_skip(-1); } function dl_song() { From 1cdb3880902ee2761d813f669841ecf3dc011607 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 6 Sep 2025 21:00:21 +0000 Subject: [PATCH 140/154] partyfuse: usernames --- bin/partyfuse.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/partyfuse.py b/bin/partyfuse.py index da7ce54e..aa3763ef 100755 --- a/bin/partyfuse.py +++ b/bin/partyfuse.py @@ -359,7 +359,7 @@ class Gateway(object): def sendreq(self, meth, path, headers, **kwargs): tid = get_tid() if self.password: - headers["Cookie"] = "=".join(["cppwd", self.password]) + headers["PW"] = self.password try: c = self.getconn(tid) @@ -1141,10 +1141,15 @@ def main(): if WINDOWS: examples.append("http://192.168.1.69:3923/music/ M:") + epi = "example:" + ex_pre + ex_pre.join(examples) + epi += """\n +NOTE: if server has --usernames enabled, then password is "username:password" +""" + ap = argparse.ArgumentParser( formatter_class=TheArgparseFormatter, description="mount a copyparty server as a local filesystem -- " + ver, - epilog="example:" + ex_pre + ex_pre.join(examples), + epilog=epi, ) # fmt: off ap.add_argument("base_url", type=str, help="remote copyparty URL to mount") From 06d2654b3f7e619b1a53d0bfa9bf43b1d2ca9192 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 6 Sep 2025 21:31:09 +0000 Subject: [PATCH 141/154] partyfuse: readdir from cache; dircache only applied to `getattr` and not `readdir` itself --- bin/partyfuse.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bin/partyfuse.py b/bin/partyfuse.py index aa3763ef..490726e1 100755 --- a/bin/partyfuse.py +++ b/bin/partyfuse.py @@ -902,9 +902,7 @@ class CPPF(Operations): return ret def _readdir(self, path, fh=None): - path = path.strip("/") - dbg("readdir %r [%s]", path, fh) - + dbg("dircache miss") ret = self.gw.listdir(path) if not self.n_dircache: return ret @@ -914,11 +912,17 @@ class CPPF(Operations): self.dircache.append(cn) self.clean_dircache() - # import pprint; pprint.pprint(ret) return ret def readdir(self, path, fh=None): - return [".", ".."] + list(self._readdir(path, fh)) + dbg("readdir %r [%s]", path, fh) + path = path.strip("/") + cn = self.get_cached_dir(path) + if cn: + ret = cn.data + else: + ret = self._readdir(path, fh) + return [".", ".."] + list(ret) def read(self, path, length, offset, fh=None): req_max = 1024 * 1024 * 8 @@ -993,7 +997,6 @@ class CPPF(Operations): if cn: dents = cn.data else: - dbg("cache miss") dents = self._readdir(dirpath) try: From 67ba5b02527b52d4e3d349ba9670b6a32bc44e9a Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 6 Sep 2025 22:12:07 +0000 Subject: [PATCH 142/154] partyfuse: suggest fuse2 --- bin/partyfuse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/partyfuse.py b/bin/partyfuse.py index 490726e1..0d88caca 100755 --- a/bin/partyfuse.py +++ b/bin/partyfuse.py @@ -6,8 +6,8 @@ __copyright__ = 2019 __license__ = "MIT" __url__ = "https://github.com/9001/copyparty/" -S_VERSION = "2.0" -S_BUILD_DT = "2024-10-01" +S_VERSION = "2.1" +S_BUILD_DT = "2025-09-06" """ mount a copyparty server (local or remote) as a filesystem @@ -99,7 +99,7 @@ except: elif MACOS: libfuse = "install https://osxfuse.github.io/" else: - libfuse = "apt install libfuse3-3\n modprobe fuse" + libfuse = "apt install libfuse2\n modprobe fuse" m = """\033[33m could not import fuse; these may help: From 3bdef75e8861241f6e18dd73edafda5e6eaeb8f3 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 6 Sep 2025 22:17:48 +0000 Subject: [PATCH 143/154] connectpage: usernames --- copyparty/httpcli.py | 17 ++++++++-- copyparty/web/svcs.html | 69 ++++++++++++++++++++++++++++++++--------- copyparty/web/svcs.js | 36 ++++++++++++++++++--- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 7dc4bc1b..dd4d958e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5024,10 +5024,20 @@ class HttpCli(object): else: rip = host + defpw = "dave:hunter2" if self.args.usernames else "hunter2" + vp = (self.uparam["hc"] or "").lstrip("/") - pw = self.ouparam.get("pw") or "hunter2" + pw = self.ouparam.get("pw") or defpw if pw in self.asrv.sesa: - pw = "hunter2" + pw = defpw + + unpw = pw + try: + un, pw = unpw.split(":") + except: + un = "" + if self.args.usernames: + un = "dave" html = self.j2s( "svcs", @@ -5041,7 +5051,10 @@ class HttpCli(object): host=html_sh_esc(host), hport=html_sh_esc(hport), aname=aname, + b_un=("<b>%s</b>" % (html_sh_esc(un),)) if un else "k", + un=html_sh_esc(un), pw=html_sh_esc(pw), + unpw=html_sh_esc(unpw), ) self.reply(html.encode("utf-8")) return True diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index 64e2e0db..f63e670f 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -31,10 +31,10 @@ <br /> <span class="os win lin mac">placeholders:</span> <span class="os win"> - {% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint + {% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint </span> <span class="os lin mac"> - {% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint + {% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint </span> {% if accs %}<a href="#" id="setpw">use real password</a>{% endif %} <a href="#" id="qr">show qr</a> @@ -54,7 +54,7 @@ <div class="os win"> <p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p> <pre> - rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %} + rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %} rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b> </pre> <ul> @@ -66,14 +66,18 @@ <p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p> <pre> + {%- if un %} + net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %} + {%- else %} net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %} + {%- endif %} </pre> </div> <div class="os lin"> <p>rclone (v1.63 or later) is recommended:</p> <pre> - rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %} + rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %} rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b> </pre> <ul> @@ -86,18 +90,28 @@ <p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p> <pre> yum install davfs2 + {%- if un %} + {% if accs %}printf '%s\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> + {%- else %} {% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> + {%- endif %} </pre> + {%- if accs %} <p>make davfs2 automount on boot:</p> <pre> + {%- if un %} + printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>" >> /etc/davfs2/secrets + {%- else %} printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets + {%- endif %} printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab </pre> + {%- endif %} <p>or the emergency alternative (gnome/gui-only):</p> <!-- gnome-bug: ignores vp --> <pre> {%- if accs %} - echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ rvp }} + echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }} {%- else %} gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }} {%- endif %} @@ -107,11 +121,11 @@ <div class="os mac"> <pre> - osascript -e ' mount volume "http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" ' + osascript -e ' mount volume "http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" ' </pre> <p>or you can open up a Finder, press command-K and paste this instead:</p> <pre> - http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }} + http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }} </pre> {%- if s %} @@ -130,14 +144,22 @@ {%- if args.ftp %} <p>connect with plaintext FTP:</p> <pre> + {%- if un %} + rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false + {%- else %} rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false + {%- endif %} rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b> </pre> {%- endif %} {%- if args.ftps %} <p>connect with TLS-encrypted FTPS:</p> <pre> + {%- if un %} + rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true + {%- else %} rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true + {%- endif %} rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b> </pre> {%- endif %} @@ -149,7 +171,11 @@ </ul> <p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p> <pre> + {%- if un %} + explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} + {%- else %} explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} + {%- endif %} </pre> </div> @@ -157,14 +183,22 @@ {%- if args.ftp %} <p>connect with plaintext FTP:</p> <pre> + {%- if un %} + rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false + {%- else %} rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false + {%- endif %} rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b> </pre> {%- endif %} {%- if args.ftps %} <p>connect with TLS-encrypted FTPS:</p> <pre> + {%- if un %} + rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true + {%- else %} rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true + {%- endif %} rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b> </pre> {%- endif %} @@ -179,7 +213,7 @@ <!-- gnome-bug: ignores vp --> <pre> {%- if accs %} - echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://k@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} + echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://{{ b_un }}@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} {%- else %} gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} {%- endif %} @@ -189,7 +223,7 @@ <div class="os mac"> <p>note: FTP is read-only on macos; please use WebDAV instead</p> <pre> - open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} + open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} </pre> </div> {%- endif %} @@ -204,7 +238,7 @@ <span class="os lin">doesn't need root</span> </p> <pre> - partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b> + partyfuse.py{% if accs %} -a <b>{{ unpw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b> </pre> {%- if s %} <ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul> @@ -217,6 +251,10 @@ {% if args.smb %} <h1>SMB / CIFS</h1> + {%- if un %} + <h2>not available on this server because <code>--usernames</code> is enabled in the server config</h2> + {%- else %} + <div class="os win"> <pre> net use <b>w:</b> \\{{ host }}\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %} @@ -235,6 +273,7 @@ open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a' </pre> {%- endif %} + {%- endif %} @@ -247,7 +286,7 @@ { "Version": "15.0.0", "Name": "copyparty", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "Headers": { - {% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url" + {% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url" }, "DestinationType": "ImageUploader, TextUploader, FileUploader", "Body": "MultipartFormData", "URL": "{response}", @@ -260,7 +299,7 @@ { "Name": "copyparty", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "Headers": { - {% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url" + {% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url" }, "DestinationType": "ImageUploader, TextUploader, FileUploader", "FileFormName": "f" } @@ -278,7 +317,9 @@ { "Name": "copyparty", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "Headers": { - {% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %} + {%- if accs %} + "pw": "<b>{{ unpw }}</b>", + {%- endif %} "accept": "json" }, "ResponseURL": "{{ '{{fileurl}}' }}", @@ -295,7 +336,7 @@ <pre class="dl" name="flameshot.sh"> #!/bin/bash - pw="<b>{{ pw }}</b>" + pw="<b>{{ unpw }}</b>" url="http{{ s }}://{{ ep }}/{{ rvp }}" filename="$(date +%Y-%m%d-%H%M%S).png" flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib diff --git a/copyparty/web/svcs.js b/copyparty/web/svcs.js index dad7faab..0b0f6b91 100644 --- a/copyparty/web/svcs.js +++ b/copyparty/web/svcs.js @@ -49,21 +49,47 @@ function setos(os) { setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk'); -var pw = ''; +var un, un0, pw, pw0, unpw, up0; function setpw(e) { ev(e); + if (!ebi('un0')) + return askpw(); + + modal.prompt('username:', '', function (v) { + if (!v) + return; + + un = v; + un0 = ebi('un0').innerHTML; + var oa = QSA('b'); + + for (var a = 0; a < oa.length; a++) + if (oa[a].innerHTML == un0) + oa[a].textContent = un; + + askpw(); + }); +} +function askpw() { modal.prompt('password:', '', function (v) { if (!v) return; pw = v; - var pw0 = ebi('pw0').innerHTML, - oa = QSA('b'); - + pw0 = ebi('pw0').innerHTML; + var oa = QSA('b'); + for (var a = 0; a < oa.length; a++) if (oa[a].innerHTML == pw0) - oa[a].textContent = v; + oa[a].textContent = pw; + if (un) { + unpw = un ? (un+':'+pw) : pw; + up0 = ebi('up0').innerHTML; + for (var a = 0; a < oa.length; a++) + if (oa[a].innerHTML == up0) + oa[a].textContent = unpw; + } add_dls(); }); } From ab562382499688420a7e7ce1440c6327ee789d8e Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sat, 6 Sep 2025 23:44:48 +0000 Subject: [PATCH 144/154] docker: fix image annotations; docker buildx imagetools inspect copyparty/ac:beta@sha256:[...] --raw would show the annotations from the base alpine image instead of ours thx to @EmilyxFox for figuring this out! --- scripts/docker/make.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/docker/make.sh b/scripts/docker/make.sh index 88758c2a..13a4a4bb 100755 --- a/scripts/docker/make.sh +++ b/scripts/docker/make.sh @@ -102,12 +102,18 @@ filt= # arm takes forever so make it top priority [ ${a::3} == arm ] && nice= || nice=-n20 + # not sure if this is necessary or if inherit-annotations=false was enough, but won't hurt + readarray -t annot < <(awk <Dockerfile.$i '/org.opencontainers.image/{sub(/[^\.]+/,"");sub(/[" \\]+$/,"");sub(/"/,"");print"--annotation";print"org"$0}') + annot+=( --annotation "org.opencontainers.image.created=$( date -u +%Y-%m-%dT%H:%M:%SZ )" ) + # --pull=never does nothing at all btw (set -x nice $nice podman build \ --squash \ --pull=never \ --from localhost/alpine-$a \ + --inherit-annotations=false \ + "${annot[@]}" \ -t copyparty-$i-$a$suf \ -f Dockerfile.$i . || (echo $? $i-$a >> err; printf '%096d\n' $(seq 1 42)) From e270fe60ede9204650e981453c9ca2d94803ba36 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 7 Sep 2025 09:02:43 +0000 Subject: [PATCH 145/154] fix uds perms with rm-sck --- copyparty/tcpsrv.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index b29b75f7..6d294336 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -305,6 +305,10 @@ class TcpSrv(object): if os.path.exists(ip): os.unlink(ip) srv.bind(ip) + if uds_gid != -1: + os.chown(ip, -1, uds_gid) + if uds_perm != -1: + os.chmod(ip, uds_perm) else: tf = "%s.%d" % (ip, os.getpid()) if os.path.exists(tf): From edafa1586ae392b33d872104c9498b157c966c47 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 7 Sep 2025 17:20:51 +0000 Subject: [PATCH 146/154] volflag to block sharing of a volume --- copyparty/__main__.py | 1 + copyparty/authsrv.py | 17 +++++++++++++++-- copyparty/cfg.py | 2 ++ copyparty/httpcli.py | 8 ++++++++ copyparty/web/browser.js | 13 ++++++++++--- copyparty/web/up2k.js | 1 + tests/util.py | 1 + 7 files changed, 38 insertions(+), 5 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 9751fb26..074f865f 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1193,6 +1193,7 @@ def add_share(ap): ap2 = ap.add_argument_group("share-url options") ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]") ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in") + 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-v", action="store_true", help="debug") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index f68a9f7b..69973ec5 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -976,6 +976,14 @@ class AuthSrv(object): self.indent = "" self.is_lxc = args.c == ["/z/initcfg"] + self._vf0b = { + "tcolor": self.args.tcolor, + "du_iwho": self.args.du_iwho, + "shr_who": self.args.shr_who if self.args.shr else "no", + } + self._vf0 = self._vf0b.copy() + self._vf0["d2d"] = True + # fwd-decl self.vfs = VFS(log_func, "", "", "", AXS(), {}) self.acct: dict[str, str] = {} # uname->pw @@ -1014,10 +1022,10 @@ class AuthSrv(object): yield prev, True def vf0(self): - return {"d2d": True, "tcolor": self.args.tcolor, "du_iwho": self.args.du_iwho} + return self._vf0.copy() def vf0b(self): - return {"tcolor": self.args.tcolor, "du_iwho": self.args.du_iwho} + return self._vf0b.copy() def idp_checkin( self, broker: Optional["BrokerCli"], uname: str, gname: str @@ -2298,6 +2306,9 @@ class AuthSrv(object): vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"]) + if not enshare: + vol.flags["shr_who"] = "no" + if vol.flags.get("og"): self.args.uqe = True @@ -2804,6 +2815,7 @@ class AuthSrv(object): "dcrop": vf["crop"], "dth3x": vf["th3x"], "u2ts": vf["u2ts"], + "shr_who": vf["shr_who"], "frand": bool(vf.get("rand")), "lifetime": vf.get("lifetime") or 0, "unlist": vf.get("unlist") or "", @@ -2818,6 +2830,7 @@ class AuthSrv(object): "have_c2flac": self.args.allow_flac, "have_c2wav": self.args.allow_wav, "have_shr": self.args.shr, + "shr_who": vf["shr_who"], "have_zip": not self.args.no_zip, "have_mv": not self.args.no_mv, "have_del": not self.args.no_del, diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 549ffa10..2fcb3d4e 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -108,6 +108,7 @@ def vf_vmap() -> dict[str, str]: "put_name", "mv_retry", "rm_retry", + "shr_who", "sort", "tail_fd", "tail_rate", @@ -350,6 +351,7 @@ flagcats = { "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', "rss": "allow '?rss' URL suffix (experimental)", "rmagic": "expensive analysis for mimetype accuracy", + "shr_who=auth": "who can create shares? no/auth/a", "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name", "ups_who=2": "restrict viewing the list of recent uploads", "zip_who=2": "restrict access to download-as-zip/tar", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index dd4d958e..d02efd0a 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5955,6 +5955,14 @@ class HttpCli(object): except: raise Pebkac(400, "you dont have all the perms you tried to grant") + zs = vfs.flags["shr_who"] + if zs == "auth" and self.uname != "*": + pass + elif zs == "a" and self.uname in vfs.axs.uadmin: + pass + else: + raise Pebkac(400, "you dont have perms to create shares from this volume") + ap, reals, _ = vfs.ls( rem, self.uname, not self.args.no_scandir, [[s_rd, s_wr, s_mv, s_del]] ) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 6c1f6184..c6cac72e 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -11807,6 +11807,7 @@ var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext), noih = /[?&]v\b/.exec(sloc0), dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0), abrt_key = "", + can_shr = false, rtt = null, ldks = [], dks = {}, @@ -14396,7 +14397,7 @@ var fileman = (function () { hdel = !(have_del && has(perms, 'delete')), hcut = !(have_mv && has(perms, 'move')), hpst = !(have_mv && has(perms, 'write')), - hshr = !(have_shr && acct != '*' && (has(perms, 'read') || has(perms, 'write'))); + hshr = !can_shr; if (!(enren || endel || encut || enpst)) hren = hdel = hcut = hpst = true; @@ -17887,7 +17888,7 @@ var treectl = (function () { fun(); } - if (window.have_shr && QS('#op_unpost.act') && (cdir.startsWith(SR + have_shr) || get_evpath().startsWith(SR + have_shr))) + if (can_shr && QS('#op_unpost.act') && (cdir.startsWith(SR + have_shr) || get_evpath().startsWith(SR + have_shr))) goto('unpost'); } @@ -18299,9 +18300,10 @@ function apply_perms(res) { if (konmai < 0) { acct = 'Ted Faro'; srvinf = 'FAS Nexus</span> // <span>57.3 EiB free of 127 EiB'; + res.shr_who = 'auth'; perms = res.perms = chk; have_up2k_idx = have_tags_idx = 1; - have_shr = have_mv = have_del = true; + have_mv = have_del = true; } var a = QS('#ops a[data-dest="up2k"]'); @@ -18366,6 +18368,11 @@ function apply_perms(res) { de = document.documentElement, tds = QSA('#u2conf td'); + shr_who = res.shr_who || shr_who; + can_shr = acct != '*' && (have_read || have_write) && ( + (shr_who == 'a' && has(perms, 'admin')) || + (shr_who == 'auth')); + clmod(de, "read", have_read); clmod(de, "write", have_write); clmod(de, "nread", !have_read); diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index a52dd172..dc2e8243 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -3427,6 +3427,7 @@ if (QS('#op_up2k.act')) goto_up2k(); apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts }); +fileman.render(); (function () { diff --git a/tests/util.py b/tests/util.py index 6acc4a42..cfb5979d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -208,6 +208,7 @@ class Cfg(Namespace): rm_retry="0/0", s_rd_sz=256 * 1024, s_wr_sz=256 * 1024, + shr_who="auth", sort="href", srch_hits=99999, SRS="/", From 422f8f624ee2075b76a95e60188fcba86df02ba2 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 7 Sep 2025 20:42:23 +0000 Subject: [PATCH 147/154] fix volflag og_ua --- copyparty/httpcli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index d02efd0a..70eedbb1 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6225,8 +6225,8 @@ class HttpCli(object): if add_og: if "th" in self.uparam or "raw" in self.uparam: og_ua = add_og = False - elif self.args.og_ua: - og_ua = add_og = self.args.og_ua.search(self.ua) + elif vn.flags["og_ua"]: + og_ua = add_og = vn.flags["og_ua"].search(self.ua) else: og_ua = False add_og = True From 98386f28f0789c86973a5c6829fddb4640ff33e2 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 7 Sep 2025 20:54:37 +0000 Subject: [PATCH 148/154] simplify og_ua logic; idk what this was *supposed* to do but what it *did* was prevent loading the full image even when the request had a good referrer (this broke viewing images in firefox at least) --- copyparty/httpcli.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 70eedbb1..42b373e5 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6224,16 +6224,13 @@ class HttpCli(object): add_og = "og" in vn.flags if add_og: if "th" in self.uparam or "raw" in self.uparam: - og_ua = add_og = False + add_og = False elif vn.flags["og_ua"]: - og_ua = add_og = vn.flags["og_ua"].search(self.ua) - else: - og_ua = False - add_og = True + add_og = vn.flags["og_ua"].search(self.ua) og_fn = "" if "v" in self.uparam: - add_og = og_ua = True + add_og = True if "b" in self.uparam: self.out_headers["X-Robots-Tag"] = "noindex, nofollow" @@ -6354,7 +6351,7 @@ class HttpCli(object): is_md = abspath.lower().endswith(".md") if add_og and not is_md: - if og_ua or self.host not in self.headers.get("referer", ""): + if self.host not in self.headers.get("referer", ""): self.vpath, og_fn = vsplit(self.vpath) vpath = self.vpath vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) From e0a92ba72d46074209a9c304eb2a01ca0429e60c Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 7 Sep 2025 22:48:31 +0000 Subject: [PATCH 149/154] fence fileshares to just those files when a share is created for just a single file, it was possible to guess other filenames in the source folder and access those files --- copyparty/authsrv.py | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 69973ec5..88aa7314 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -431,6 +431,8 @@ class VFS(object): self.get_dbv = self._get_dbv self.ls = self._ls + self.canonical = self._canonical + self.dcanonical = self._dcanonical def __repr__(self) -> str: return "VFS(%s)" % ( @@ -624,7 +626,7 @@ class VFS(object): vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem) return dbv, vrem - def canonical(self, rem: str, resolve: bool = True) -> str: + def _canonical(self, rem: str, resolve: bool = True) -> str: """returns the canonical path (fully-resolved absolute fs path)""" ap = self.realpath if rem: @@ -632,7 +634,7 @@ class VFS(object): return absreal(ap) if resolve else ap - def dcanonical(self, rem: str) -> str: + def _dcanonical(self, rem: str) -> str: """resolves until the final component (filename)""" ap = self.realpath if rem: @@ -641,6 +643,44 @@ class VFS(object): ad, fn = os.path.split(ap) return os.path.join(absreal(ad), fn) + def _canonical_shr(self, rem: str, resolve: bool = True) -> str: + """returns the canonical path (fully-resolved absolute fs path)""" + ap = self.realpath + if rem: + ap += "/" + rem + + rap = absreal(ap) + if self.shr_files: + assert self.shr_src # !rm + vn, rem = self.shr_src + chk = absreal(os.path.join(vn.realpath, rem)) + if chk != rap: + # not the dir itself; assert file allowed + ad, fn = os.path.split(rap) + if chk != ad or fn not in self.shr_files: + return "\n\n" + + return rap if resolve else ap + + def _dcanonical_shr(self, rem: str) -> str: + """resolves until the final component (filename)""" + ap = self.realpath + if rem: + ap += "/" + rem + + ad, fn = os.path.split(ap) + ad = absreal(ad) + if self.shr_files: + assert self.shr_src # !rm + vn, rem = self.shr_src + chk = absreal(os.path.join(vn.realpath, rem)) + if chk != absreal(ap): + # not the dir itself; assert file allowed + if ad != chk or fn not in self.shr_files: + return "\n\n" + + return os.path.join(ad, fn) + def _ls_nope( self, *a, **ka ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]: @@ -2751,6 +2791,8 @@ class AuthSrv(object): shn.shr_files = set(fns) shn.ls = shn._ls_shr + shn.canonical = shn._canonical_shr + shn.dcanonical = shn._dcanonical_shr else: shn.ls = shn._ls From c47c708433fd1b109289d88e6ce8acbb3b11c872 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 7 Sep 2025 23:00:05 +0000 Subject: [PATCH 150/154] v1.19.8 --- copyparty/__version__.py | 4 ++-- copyparty/web/browser.css | 5 ++++- docs/changelog.md | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 2b7fbbf9..eb605668 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 19, 7) +VERSION = (1, 19, 8) CODENAME = "usernames" -BUILD_DT = (2025, 8, 28) +BUILD_DT = (2025, 9, 7) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index a40c72e2..35c822cc 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -3496,7 +3496,6 @@ html.ez { html.e { text-shadow: none; } - html.e #files, html.e #u2conf input[type="checkbox"]:hover + label, html.e .tgl.btn.on:hover, @@ -3596,6 +3595,10 @@ html.e #srv_info { display: flex; align-items: center; } +html.e #acc_info span.warn, +html.e #acc_info a { + color: var(--white); +} html.e #flogout:before { padding-left: 0.2em; padding-right: 0.4em; diff --git a/docs/changelog.md b/docs/changelog.md index d83fc584..ac773418 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,28 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +# 2025-0828-2014 `v1.19.7` chdir + +## 🧪 new features + +* new option `chdir` to change the PWD (process working-directory) before volumes are mapped 14555d58 + +## 🩹 bugfixes + +* fix using empty folders as statefile storage ([v1.19.6](https://github.com/9001/copyparty/releases/tag/v1.19.6) made this a bit too strict) 0d96786e +* holding I/K to scroll through folders quickly now works better 914686ec + +## 🔧 other changes + +* #717 docker: fix the image repo metadata (thx @EmilyxFox!) 6f087117 +* docker: change `$HOME` to `/state` 01cf20a0 d1f75229 + * and use the new `chdir` option to preserve old config-file semantics 14555d58 + * helps avoid statefiles accidentally landing in `/w` as a consequence of misconfiguration + +## 🌠 fun facts + +* this release was made at [RevSpace NL](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250828_202820075.jpg?cache) + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ # 2025-0827-2038 `v1.19.6` auth-precedence From 75b0b312a4ecf1c468b529a2cfe33adcd93ec26b Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Sun, 7 Sep 2025 23:16:36 +0000 Subject: [PATCH 151/154] update pkgs to 1.19.8 --- 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 49125029..a83a6698 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.19.7" +pkgver="1.19.8" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -23,7 +23,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=("229476fc111de22a46d56c564d5e615bce6ce222321bcb73b336a3ead03d01f9") +sha256sums=("3143ba5216c8d4cf1fbc58fa08c6ecef955de04b5e34b3910ab0b71cffec88ef") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index f491eaee..d1512976 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.19.7 +pkgver=1.19.8 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -20,7 +20,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=("229476fc111de22a46d56c564d5e615bce6ce222321bcb73b336a3ead03d01f9") +sha256sums=("3143ba5216c8d4cf1fbc58fa08c6ecef955de04b5e34b3910ab0b71cffec88ef") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index 02c7e1ba..5ec6e679 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.19.7/copyparty-1.19.7.tar.gz", - "version": "1.19.7", - "hash": "sha256-IpR2/BEd4ipG1WxWTV5hW85s4iIyG8tzszaj6tA9Afk=" + "url": "https://github.com/9001/copyparty/releases/download/v1.19.8/copyparty-1.19.8.tar.gz", + "version": "1.19.8", + "hash": "sha256-MUO6UhbI1M8fvFj6CMbs75Vd4EteNLORCrC3HP/siO8=" } \ No newline at end of file From 25749b4b5fccfd3f0fc226fb46d574ffd55d1ee8 Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Mon, 8 Sep 2025 10:08:34 +0200 Subject: [PATCH 152/154] accept empty files through bup; closes #775 --- copyparty/httpcli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 42b373e5..26b85a18 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3419,8 +3419,6 @@ class HttpCli(object): sz, sha_hex, sha_b64 = copier( p_data, f, hasher, max_sz, self.args.s_wr_slp ) - if sz == 0: - raise Pebkac(400, "empty files in post") finally: f.close() From e09f3c9e2c3dccf8f3912539e04dd840b10b51ee Mon Sep 17 00:00:00 2001 From: ed <s@ocv.me> Date: Mon, 8 Sep 2025 20:21:12 +0000 Subject: [PATCH 153/154] shutil: ignore errors from copystat in copy2; ntfs on linux can be picky about cloning mtime onto a new file; generally we don't care if that fails, however, we also want the speedup that CopyFile2 can offer, so cannot use copyfile directly this avoids the following issue: up2k:3537 <_symlink>: shutil.copy2(fsenc(csrc), fsenc(dst)) shutil:437 <copy2>: copystat(src, dst, follow_symlinks=follow_sym[...] shutil:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, s[...] [PermissionError] [Errno 1] Operation not permitted, '/windows/videos' --- copyparty/up2k.py | 9 +++++---- copyparty/util.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 3acf3918..3f8e8f95 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -60,6 +60,7 @@ from .util import ( sfsenc, spack, statdir, + trystat_shutil_copy2, ub64enc, unhumanize, vjoin, @@ -2768,7 +2769,7 @@ class Up2k(object): cur.close() db.close() - shutil.copy2(fsenc(db_path), fsenc(bak)) + trystat_shutil_copy2(self.log, fsenc(db_path), fsenc(bak)) return self._orz(db_path) def _read_ver(self, cur: "sqlite3.Cursor") -> Optional[int]: @@ -3591,7 +3592,7 @@ class Up2k(object): t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)" self.log(t, 1) raise Exception(t % (src, fsrc, dst)) - shutil.copy2(fsenc(csrc), fsenc(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) @@ -4427,7 +4428,7 @@ class Up2k(object): b1, b2 = fsenc(sabs), fsenc(dabs) is_link = os.path.islink(b1) # due to _relink try: - shutil.copy2(b1, b2) + trystat_shutil_copy2(self.log, b1, b2) except: try: wunlink(self.log, dabs, dvn.flags) @@ -4733,7 +4734,7 @@ class Up2k(object): b1, b2 = fsenc(sabs), fsenc(dabs) is_link = os.path.islink(b1) # due to _relink try: - shutil.copy2(b1, b2) + trystat_shutil_copy2(self.log, b1, b2) except: try: wunlink(self.log, dabs, dvn.flags) diff --git a/copyparty/util.py b/copyparty/util.py index c476a2ea..da6bbce4 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -2639,6 +2639,24 @@ def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> os.fchown(fno, vf["uid"], vf["gid"]) +def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes: + try: + return shutil.copy2(src, dst) + except: + # ignore failed mtime on linux+ntfs; for example: + # shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks) + # shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns), + # [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos' + _, _, tb = sys.exc_info() + for _, _, fun, _ in traceback.extract_tb(tb): + if fun == "copystat": + if log: + t = "warning: failed to retain some file attributes (timestamp and/or permissions) during copy from %r to %r:\n%s" + log(t % (src, dst, min_ex()), 3) + return dst # close enough + raise + + def _fs_mvrm( log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any] ) -> bool: From 52438bcc0d8babfdf8f7f6cc245a9f3321b47d48 Mon Sep 17 00:00:00 2001 From: daimond113 <contact@daimond113.com> Date: Mon, 8 Sep 2025 22:05:35 +0200 Subject: [PATCH 154/154] update polish pluralization --- copyparty/web/browser.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index c6cac72e..9592634c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -7677,11 +7677,14 @@ var Ls = { "clipped": "skopiowano do schowka", "ht_s1": "sekunda", - "ht_s2": "sekund(y)", + "ht_s2": "sekundy", + "ht_s5": "sekund", "ht_m1": "minuta", "ht_m2": "minuty", + "ht_m5": "minut", "ht_h1": "godzina", "ht_h2": "godziny", + "ht_h5": "godzin", "ht_d1": "dzień", "ht_d2": "dni", "ht_and": " i ",