Compare commits

...

102 Commits

Author SHA1 Message Date
ed
c1c0ecca13 v1.15.7 2024-10-13 22:44:57 +00:00
ed
ee62836383 bitflip logging 2024-10-13 22:37:35 +00:00
ed
705f598b1a up2k non-e2d fixes:
* respect noforget when loading snaps
* ...but actually forget deleted files otherwise
* insert empty need/hash as necessary
2024-10-13 22:10:27 +00:00
ed
414de88925 u2c v2.2 2024-10-13 22:07:41 +00:00
ed
53ffd245dd u2c: fix progress indicator for resumed uploads 2024-10-13 22:07:07 +00:00
ed
cf1b756206 u2c: option to list chunk hashes 2024-10-13 22:06:02 +00:00
ed
22b58e31ef unpost: authed users can see anon on same ip 2024-10-13 22:00:15 +00:00
ed
b7f9bf5a28 cidr-based autologin 2024-10-13 21:56:26 +00:00
ed
aba680b6c2 update pkgs to 1.15.6 2024-10-11 23:16:24 +00:00
ed
fabada95f6 v1.15.6 2024-10-11 22:56:10 +00:00
ed
9ccd8bb3ea support viewing dotfile docs; closes #104 2024-10-11 22:06:43 +00:00
ed
1d68acf8f0 add preadme.md; closes #105 2024-10-11 21:52:44 +00:00
ed
1e7697b551 misc cleanup;
* more typos
* python 3.13 deprecations
2024-10-11 20:46:40 +00:00
ed
4a4ec88d00 up2k: fix hs after bitflips / net-glitch
chunk stitching could cause handshakes to initiate
a new upload rather than resume an ongoing one
2024-10-11 19:48:44 +00:00
ed
6adc778d62 fix a buttload of typos 2024-10-11 18:58:14 +00:00
ed
6b7ebdb7e9 upgrade old snaps to dwrk + fix ptop
ptop would be wrong if a volume was moved on-disk since last run
2024-10-09 06:05:55 +00:00
ed
3d7facd774 add option to entirely disable dedup
global-option `--no-clone` / volflag `noclone` entirely disables
serverside deduplication; clients will then fully upload dupe files

can be useful when `--safe-dedup=1` is not an option due to other
software tampering with the on-disk files, and your filesystem has
prohibitively slow or expensive reads
2024-10-08 21:27:19 +00:00
ed
eaee1f2cab update pkgs to 1.15.5 2024-10-05 18:20:20 +00:00
ed
ff012221ae v1.15.5 2024-10-05 18:03:04 +00:00
ed
c398553748 pkgres: fix multiprocessing 2024-10-05 17:32:08 +00:00
ed
3ccbcf6185 update pkgs to 1.15.4 2024-10-04 23:56:45 +00:00
ed
f0abc0ef59 v1.15.4 2024-10-04 23:19:28 +00:00
ed
a99fa3375d the impresources.files traversible is not threadsafe 2024-10-04 22:37:29 +00:00
ed
22c7e09b3f small fixes;
* make-sfx: delete failed deps downloads
* tlcheck: detect untranslated strings
2024-10-04 20:56:16 +00:00
ed
0dfe1d5b35 toast countdown bar 2024-10-04 19:29:54 +00:00
ed
a99a3bc6d7 audio-player: fix compact-mode rendering glitch on narrow screens 2024-10-04 18:15:18 +00:00
ed
9804f25de3 add option for natural sorting; thx @oshiteku 2024-10-04 00:30:04 +00:00
ed
ae98200660 og: support filekeys 2024-10-03 23:52:11 +00:00
ed
e45420646f share folders as qr-codes 2024-10-03 23:14:06 +00:00
ed
21be82ef8b fix #101 (show logues even if dotfiles are hidden) 2024-10-03 22:19:32 +02:00
ed
001afe00cb i18n: time plurals 2024-10-03 07:38:33 +00:00
ed
19a5985f29 allow uploading logues; closes #100 2024-10-02 23:16:59 +00:00
ed
2715ee6c61 fix confusing toast on F2 with nothing selected (#100) 2024-10-02 23:11:29 +00:00
ed
dc157fa28f webdav: support explicit <allprop/> (WinSCP) 2024-10-02 22:28:23 +00:00
ed
1ff14b4e05 optimizations, failsafes, formatting 2024-10-02 21:59:53 +00:00
ed
480ac254ab webdav: show toplevel volumes when root is unmapped
previously, only real folders could be listed by a webdav client;
a server which does not have any filesystem paths mapped to `/`
would cause clients to panic when trying to list the server root

now, assuming volumes `/foo` and `/bar/qux` exist, when accessing `/`
the user will see `/foo` but not `/bar` due to limitations in `walk`,
and `qux` will only appear when viewing `/bar`

a future rework of the recursion logic should further improve this
2024-10-02 21:12:58 +00:00
ed
4b95db81aa thx keth 2024-10-01 22:37:50 +00:00
ed
c81e898435 partyfuse: also support mounting nginx, iis
these additional parsers are not included in the sfx-embedded
copy of partyfuse.py; grab it from github when necessary
2024-10-01 22:37:07 +00:00
ed
f1646b96ca dist: strip some pointless code 2024-10-01 18:35:36 +00:00
ed
44f2b63e43 partyfuse: embed fuse.py into sfx 2024-10-01 18:27:42 +00:00
ed
847a2bdc85 partyfuse: bump datacache chunksize
previous approach:
* cache 64K on first read
* cache 1M on subsequent intersecting reads

new approach:
* cache 64K on first read
* cache 1M on the next intersecting read
* cache 8M on subsequent intersecting reads
* cache 4M on standalone reads at offsets >1M

improves performance by 50% on windows
and should help on high-latency connections
2024-10-01 17:15:35 +00:00
ed
03f0f99469 partyfuse: fix extremely slow dircache lookups
the cache was a list of files instead of a dict... dude

also adds a max-num dircache limit
in addition to the expiration time
2024-10-01 17:07:28 +00:00
ed
3900e66158 partyfuse: modernize html parser (just in case) 2024-10-01 17:00:17 +00:00
ed
3dff6cda40 partyfuse: normalize naming in parsers 2024-10-01 16:55:00 +00:00
ed
73d05095b5 partyfuse: misc correctness;
* support more unix envs with granular fuse config
* generated URLs were OK but technically incorrect
2024-10-01 16:49:39 +00:00
ed
fcdc1728eb #102: make UI translation easier in docker 2024-10-01 00:04:07 +00:00
ed
8b942ea237 partyfuse: cleanup logging and exceptions
windows runs 50% faster with recentlog on infos too...
2024-09-29 23:19:33 +00:00
ed
88a1c5ca5d optimize non-e2d ram usage down to 10% or so
drop chunk-hashes in the up2k snap, plus other insignificant attribs
to reduce both the snapfile size and the ram usage by about 90%

reduces startup/shutdown time by a lot since there's less to serdes
(does not affect -e2d which was already optimal)

other changes:

* improve incoming-eta accuracy when the initial handshake
   was made a long time before the upload actually started

* move the list of incoming files in the controlpanel to the top
2024-09-27 21:11:10 +00:00
ed
047176b297 py2 fix 2024-09-27 21:06:01 +00:00
ed
dc4d0d8e71 smb: upto 2x faster; but still very buggy:
* do not absreal paths unless necessary
* do not determine username if no users configured
* impacket 0.12 fixed the foldersize limit, but now
   you get extremely poor performance in large folders
   so the previous workaround is still default-enabled
2024-09-27 17:09:48 +00:00
ed
b9c5c7bbde u2c: early exclude + fix py2 perf
* don't traverse into excluded folders
* 3.5x faster on python2.7
2024-09-23 17:20:04 +00:00
ed
9daeed923f u2c: remove all deps to become 3x faster on small files
reduces performance on python 2.7, but that is ok

also fixes `unknown encoding: idna` due to cpython race
2024-09-22 18:07:36 +00:00
ed
66b260cea9 pkgres: fix tiny leak in template loader 2024-09-20 22:25:36 +00:00
ed
58cf01c2ad fix linter warnings 2024-09-20 22:24:39 +00:00
ed
d866841c19 pkgres:
* pyz: yeet the resource tar which is now pointless thanks to pkgres
* cache impresource stuff because pyz lookups are Extremely slow
* prefer tx_file when possible for slightly better performance
* use hardcoded list of expected resources instead of dynamic
   discovery at runtime; much simpler and probably safer
* fix some forgotten resources (copying.txt, insecure.pem)
* fix loading jinja templates on windows
2024-09-19 22:04:49 +00:00
Shiz
a462a644fb Python 3.7 package resources support (#98)
add support for reading webdeps and jinja-templates using either
importlib_resources or pkg_resources, which removes the need for
extracting these to a temporary folder on the filesystem

* util: add helper functions to abstract embedded resource access
* http*: serve embedded resources through resource abstraction
* main: check webdeps through resource abstraction
* httpconn: remove unused method `respath(name)`
* use __package__ to find package resources
* util: use importlib_resources backport if available
* pass E.pkg as module object for importlib_resources compatibility
* util: add pkg_resources compatibility to resource abstraction
2024-09-19 09:00:34 +00:00
ed
678675a9a6 fix prometheus metrics; broke in 609c5921 2024-09-16 21:04:58 +00:00
ed
de9069ef1d update pkgs to 1.15.3 2024-09-16 01:20:16 +00:00
ed
c0c0a1a83a v1.15.3 2024-09-16 01:07:50 +00:00
ed
1d004b6dbd update pkgs to 1.15.2 2024-09-16 00:44:48 +00:00
ed
b90e1200d7 v1.15.2 2024-09-16 00:20:20 +00:00
ed
4493a0a804 misc mojibake filename support 2024-09-16 00:12:49 +00:00
ed
58835b2b42 ux bugfixes:
* show media tags in shares
* html hydrator assumed a folder named `foo.txt` was a doc
* due to sessions, use `pwd` as password placeholder on services
2024-09-15 23:37:24 +00:00
ed
427597b603 show total directory size in listings
sizes are computed during `-e2ds` indexing, and new uploads
are counted, but a rescan is necessary after a move or delete
2024-09-15 23:01:18 +00:00
ed
7d64879ba8 more optimizations,
* 5% less cpu load from clients fetching thumbnails
* and slight improvement to up2k stuff
2024-09-15 17:46:43 +00:00
ed
bb715704b7 ren_open was too fancy 2024-09-15 14:39:35 +00:00
ed
d67e9cc507 sqlite and misc optimizations:
* exponentially slow upload handshakes caused by lack of rd+fn
   sqlite index; became apparent after a volume hit 200k files
* listing big folders 5% faster due to `_quotep3b`
* optimize `unquote`, 20% faster but only used rarely
* reindex on startup 150x faster in some rare cases
   (same filename in MANY folders)

the database is now around 10% larger (likely worst-case)
2024-09-15 13:18:43 +00:00
ed
2927bbb2d6 strip dev-only asserts at build stage 2024-09-14 22:17:35 +00:00
ed
0527b59180 cosmetic: only print hostname warning once 2024-09-14 20:37:56 +00:00
ed
a5ce1032d3 tlnote + nginx unix-socket example 2024-09-12 21:42:33 +00:00
ed
1c2acdc985 add flameshot client example 2024-09-11 20:56:38 +00:00
ed
4e75534ef8 optimize BrokerThr, 7x faster:
reduce the overhead of function-calls from the client thread
to the svchub singletons (up2k, thumbs, metrics) down to 14%

and optimize up2k chunk-receiver to spend 5x less time bookkeeping
which restores up2k performance to before introducing incoming-ETA
2024-09-11 20:37:10 +00:00
ultwcz
7a573cafd1 fix: translation: Check the newly added Chinese translation (#97) 2024-09-11 19:03:53 +00:00
ed
844194ee29 incoming-ETA: improve accuracy 2024-09-11 06:56:12 +00:00
ed
609c5921d4 list incoming files + ETA in controlpanel 2024-09-10 21:24:05 +00:00
ed
c79eaa089a update pkgs to 1.15.1 2024-09-09 23:55:37 +00:00
ed
e9d962f273 v1.15.1 2024-09-09 23:43:43 +00:00
ed
b5405174ec add login sessions 2024-09-09 23:39:20 +00:00
ed
6eee601521 fix u2c --ow (overwrite/replace)
the u2c flag to overwrite files on the server became no-op in v1.13.8
2024-09-09 19:40:38 +00:00
ed
2fac2bee7c update pkgs to 1.15.0 2024-09-08 20:02:25 +00:00
ed
c140eeee6b v1.15.0 2024-09-08 19:25:46 +00:00
ed
c5988a04f9 up2k.js: bump handshake timeout for safededup 2024-09-08 18:06:37 +00:00
ed
a2e0f98693 disable upload deduplication by default;
dedup is still encouraged and fully supported, but
being default-enabled has caused too many surprises

enabling `--dedup` restores the previous default behavior

also renames `--never-symlink` to `--hardlink-only`
2024-09-08 17:09:14 +00:00
ed
1111153f06 test dedup relinking 2024-09-08 12:55:27 +00:00
ed
e5a836cb7d og: fix links to textfiles 2024-09-08 12:12:34 +00:00
ed
b0de84cbc5 db-verify: support newlines in filenames + flag 404s 2024-09-08 00:44:22 +00:00
ed
cbb718e10d css fixes:
* improve hotdog-stand theme
* fix up2k tabs glow (went poof in a syntax error)
2024-09-07 19:29:40 +00:00
ed
b5ad9369fe confine xlink behavior behind its volflag
symlinks between volumes will only be created if xlink is
enabled, so such symlinks should be ignored if xlink is
disabled, as they might originate from other software

this prevents accidental rewriting of non-dedup symlinks
2024-09-07 19:17:32 +00:00
ed
4401de0413 fix mv with --no-dedup in volumes with dupes;
if --no-dedup was enabled in a volume which already contained
symlinked duplicate files, renaming/moving folders could fail

this is due to folder contents being moved one file at a time
(which is how symlink breakage is prevented) except the links
are moved assuming the final directory layout, meaning they
may be intermittently broken during the movie

with no-dedup, the symlinks are converted into full files as
each symlink is encountered, but a temporarily broken symlink
would crash the procedure

fix this by giving `_symlink` a new parameter `fsrc`
which is a known valid inode for data copying purposes
2024-09-07 00:47:12 +00:00
ed
6e671c5245 verify on-disk contents before dedup;
previously, the assumption was made that the database and filesystem
would not desync, and that an upload could safely be substituted with
a symlink to an existing copy on-disk, assuming said copy still
existed on-disk at all

this is fine if copyparty is the only software that makes changes to
the filesystem, but that is a shitty assumption to make in hindsight

add `--safe-dedup` which takes a "safety level", and by default (50)
it will no longer blindly expect that the filesystem has not been
altered through other means; the file contents will now be hashed
and compared to the database

deduplication can be much slower as a result, but definitely worth it
as this avoids some potentially very unpleasant surprises

the previous behavior can be restored with `--safe-dedup 1`
2024-09-06 19:08:14 +00:00
ed
08848be784 u2c: add hashgen mode + fix shutdown lag 2024-09-06 00:31:25 +00:00
ed
b599fbae97 use local timezone in log messages; closes #96
timezone can be changed with `export TZ=Europe/Oslo` before launch

using naive timestamps like this appears to be safe as of 3.13-rc1,
no deprecation warnings, just a tiny bit slower than assuming UTC
2024-09-05 19:31:33 +00:00
ed
a8dabc99f6 add more translations 2024-09-04 23:46:32 +00:00
ed
f1130db131 fix confusing message when uploading dupes
due to deduplication, it is intentionally impossible to
upload several identical copies of a file in parallel

by default, the up2k client will upload files sorted by
size, which usually leads to dupes being grouped together,
and it will try to do just that

this is by design, as it improves performance on average,
but it also shows the confusing (but technically-correct)
message "resume the partial upload into the original path"

fix this with a more appropriate message

note that this approach was selected in favor of pausing
handshakes while the initial copy finishes uploading,
because that could severely reduce upload performance
by preventing optimal use of multiple connections
2024-09-04 22:03:26 +00:00
ed
735ec35546 update pkgs to 1.14.4 2024-09-02 01:21:07 +00:00
ed
5a009a2a64 v1.14.4 2024-09-02 01:08:41 +00:00
ed
d9e9526247 fix js typo (could panic on network glitches) 2024-09-02 00:58:15 +00:00
ed
5a8c3b8be0 optimize test_httpcli.py too, from 1.64 to 1.51s 2024-08-31 22:03:06 +00:00
ed
1c9c17fb9b optimize test_dedup.py
* 7.71s originally
* 4.51s with fstab reuse
* 4.34s without db_wd
* 4.02s with no pp start
* 3.73s with Cfg reuse
2024-08-31 21:54:47 +00:00
ed
7f82449179 changelog: cleanup historic entries 2024-08-31 20:39:37 +00:00
ed
e455ec994e logo tweaks (kerning, footer-slant) 2024-08-31 20:37:58 +00:00
ed
c111027420 update pkgs to 1.14.3 2024-08-30 23:29:47 +00:00
91 changed files with 4872 additions and 1488 deletions

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ copyparty/res/COPYING.txt
copyparty/web/deps/
srv/
scripts/docker/i/
scripts/deps-docker/uncomment.py
contrib/package/arch/pkg/
contrib/package/arch/src/

170
README.md
View File

@@ -1,4 +1,4 @@
<img src="docs/logo.svg" width="250" align="right"/>
<img src="https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg" width="250" align="right"/>
### 💾🎉 copyparty
@@ -43,6 +43,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [unpost](#unpost) - undo/delete accidental uploads
* [self-destruct](#self-destruct) - uploads can be given a lifetime
* [race the beam](#race-the-beam) - download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm))
* [incoming files](#incoming-files) - the control-panel shows the ETA for all incoming files
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
* [shares](#shares) - share a file or folder by creating a temporary link
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
@@ -65,7 +66,8 @@ turn almost any device into a file server with resumable uploads/downloads using
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
* [browser ux](#browser-ux) - tweaking the ui
* [opengraph](#opengraph) - discord and social-media embeds
* [file indexing](#file-indexing) - enables dedup and music search ++
* [file deduplication](#file-deduplication) - enable symlink-based upload deduplication
* [file indexing](#file-indexing) - enable music search, upload-undo, and better dedup
* [exclude-patterns](#exclude-patterns) - to save some time
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
* [periodic rescan](#periodic-rescan) - filesystem monitoring
@@ -78,12 +80,14 @@ turn almost any device into a file server with resumable uploads/downloads using
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
* [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)
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
* [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 dont wanna be indexed
* [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed
* [themes](#themes)
* [complete examples](#complete-examples)
* [listen on port 80 and 443](#listen-on-port-80-and-443) - become a *real* webserver
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
* [real-ip](#real-ip) - teaching copyparty how to see client IPs
* [prometheus](#prometheus) - metrics/stats can be enabled
@@ -112,7 +116,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [https](#https) - both HTTP and HTTPS are accepted
* [recovering from crashes](#recovering-from-crashes)
* [client crashes](#client-crashes)
* [frefox wsod](#frefox-wsod) - firefox 87 can crash during uploads
* [firefox wsod](#firefox-wsod) - firefox 87 can crash during uploads
* [HTTP API](#HTTP-API) - see [devnotes](./docs/devnotes.md#http-api)
* [dependencies](#dependencies) - mandatory deps
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
@@ -239,7 +243,7 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually)
* ☑ multilingual UI (english, norwegian, [add your own](./docs/rice/#translations)))
* ☑ multilingual UI (english, norwegian, chinese, [add your own](./docs/rice/#translations)))
* ☑ SPA (browse while uploading)
* server indexing
* ☑ [locate files by contents](#file-search)
@@ -579,7 +583,7 @@ it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video f
* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg
* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
audio files are converted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
* the order is significant, so if both `cover.png` and `folder.jpg` exist in a folder, it will pick the first matching `--th-covers` entry (`folder.jpg`)
@@ -665,7 +669,7 @@ see [up2k](./docs/devnotes.md#up2k) for details on how it works, or watch a [dem
**protip:** if you enable `favicon` in the `[⚙️] settings` tab (by typing something into the textbox), the icon in the browser tab will indicate upload progress -- also, the `[🔔]` and/or `[🔊]` switches enable visible and/or audible notifications on upload completion
the up2k UI is the epitome of polished inutitive experiences:
the up2k UI is the epitome of polished intuitive experiences:
* "parallel uploads" specifies how many chunks to upload at the same time
* `[🏃]` analysis of other files should continue while one is uploading
* `[🥔]` shows a simpler UI for faster uploads from slow devices
@@ -714,7 +718,7 @@ you can unpost even if you don't have regular move/delete access, however only f
### self-destruct
uploads can be given a lifetime, afer which they expire / self-destruct
uploads can be given a lifetime, after which they expire / self-destruct
the feature must be enabled per-volume with the `lifetime` [upload rule](#upload-rules) which sets the upper limit for how long a file gets to stay on the server
@@ -730,11 +734,18 @@ download files while they're still uploading ([demo video](http://a.ocv.me/pub/g
requires the file to be uploaded using up2k (which is the default drag-and-drop uploader), alternatively the command-line program
### incoming files
the control-panel shows the ETA for all incoming files , but only for files being uploaded into volumes where you have read-access
![copyparty-cpanel-upload-eta-or8](https://github.com/user-attachments/assets/fd275ffa-698c-4fca-a307-4d2181269a6a)
## file manager
cut/paste, rename, and delete files/folders (if you have permission)
file selection: click somewhere on the line (not the link itsef), then:
file selection: click somewhere on the line (not the link itself), then:
* `space` to toggle
* `up/down` to move
* `shift-up/down` to move-and-select
@@ -768,6 +779,7 @@ semi-intentional limitations:
* cleanup of expired shares only works when global option `e2d` is set, and/or at least one volume on the server has volflag `e2d`
* only folders from the same volume are shared; if you are sharing a folder which contains other volumes, then the contents of those volumes will not be available
* related to [IdP volumes being forgotten on shutdown](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#idp-volumes-are-forgotten-on-shutdown), any shares pointing into a user's IdP volume will be unavailable until that user makes their first request after a restart
* no option to "delete after first access" because tricky
* when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
* browsers wouldn't be able to resume a broken download unless the requester's IP gets allowlisted for X minutes (ref. tricky)
@@ -927,6 +939,8 @@ see [./srv/expand/](./srv/expand/) for usage and examples
* files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence)
* and `PREADME.md` / `preadme.md` is shown above directory listings unless `--no-readme` or `.prologue.html`
* `README.md` and `*logue.html` can contain placeholder values which are replaced server-side before embedding into directory listings; see `--help-exp`
@@ -978,7 +992,11 @@ uses [multicast dns](https://en.wikipedia.org/wiki/Multicast_DNS) to give copypa
all enabled services ([webdav](#webdav-server), [ftp](#ftp-server), [smb](#smb-server)) will appear in mDNS-aware file managers (KDE, gnome, macOS, ...)
the domain will be http://partybox.local if the machine's hostname is `partybox` unless `--name` specifies soemthing else
the domain will be `partybox.local` if the machine's hostname is `partybox` unless `--name` specifies something else
and the web-UI will be available at http://partybox.local:3923/
* if you want to get rid of the `:3923` so you can use http://partybox.local/ instead then see [listen on port 80 and 443](#listen-on-port-80-and-443)
### ssdp
@@ -1004,7 +1022,7 @@ print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/
* `--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
it uses the server hostname if [mdns](#mdns) is enbled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain
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
## ftp server
@@ -1029,7 +1047,7 @@ some recommended FTP / FTPS clients; `wark` = example password:
## webdav server
with read-write support, supports winXP and later, macos, nautilus/gvfs ... a greay way to [access copyparty straight from the file explorer in your OS](#mount-as-drive)
with read-write support, supports winXP and later, macos, nautilus/gvfs ... a great way to [access copyparty straight from the file explorer in your OS](#mount-as-drive)
click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos
@@ -1106,12 +1124,12 @@ some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
* [shadowing](#shadowing) probably works as expected but no guarantees
and some minor issues,
* clients only see the first ~400 files in big folders; [impacket#1433](https://github.com/SecureAuthCorp/impacket/issues/1433)
* clients only see the first ~400 files in big folders;
* this was originally due to [impacket#1433](https://github.com/SecureAuthCorp/impacket/issues/1433) which was fixed in impacket-0.12, so you can disable the workaround with `--smb-nwa-1` but then you get unacceptably poor performance instead
* hot-reload of server config (`/?reload=cfg`) does not include the `[global]` section (commandline args)
* listens on the first IPv4 `-i` interface only (default = :: = 0.0.0.0 = all)
* login doesn't work on winxp, but anonymous access is ok -- remove all accounts from copyparty config for that to work
* win10 onwards does not allow connecting anonymously / without accounts
* on windows, creating a new file through rightclick --> new --> textfile throws an error due to impacket limitations -- hit OK and F5 to get your file
* python3 only
* slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)
@@ -1133,8 +1151,8 @@ authenticate with one of the following:
tweaking the ui
* set default sort order globally with `--sort` or per-volume with the `sort` volflag; specify one or more comma-separated columns to sort by, and prefix the column name with `-` for reverse sort
* the column names you can use are visible as tooltips when hovering over the column headers in the directory listing, for example `href ext sz ts tags/.up_at tags/Cirle tags/.tn tags/Artist tags/Title`
* to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`
* the column names you can use are visible as tooltips when hovering over the column headers in the directory listing, for example `href ext sz ts tags/.up_at tags/Circle tags/.tn tags/Artist tags/Title`
* to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Circle,tags/.tn,tags/Artist,tags/Title,href`
* to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
see [./docs/rice](./docs/rice) for more, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag, or to add your own translation
@@ -1150,14 +1168,48 @@ note that this disables hotlinking because the opengraph spec demands it; to sne
you can also hotlink files regardless by appending `?raw` to the url
NOTE: because discord (and maybe others) strip query args such as `?raw` in opengraph tags, any links which require a filekey or dirkey will not work
if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object)
## file deduplication
enable symlink-based upload deduplication globally with `--dedup` or per-volume with volflag `dedup`
by default, when someone tries to upload a file that already exists on the server, the upload will be politely declined, and the server will copy the existing file over to where the upload would have gone
if you enable deduplication with `--dedup` then it'll create a symlink instead of a full copy, thus reducing disk space usage
* on the contrary, if your server is hooked up to s3-glacier or similar storage where reading is expensive, and you cannot use `--safe-dedup=1` because you have other software tampering with your files, so you want to entirely disable detection of duplicate data instead, then you can specify `--no-clone` globally or `noclone` as a volflag
**warning:** when enabling dedup, you should also:
* enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended
* ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below
it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to)
by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file
you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`;
advantages of using hardlinks:
* hardlinks are more compatible with other software; they behave entirely like regular files
* you can safely move and rename files using other file managers
* symlinks need to be managed by copyparty to ensure the destinations remain correct
advantages of using symlinks (default):
* each symlink can have its own last-modified timestamp, but a single timestamp is shared by all hardlinks
* symlinks make it more obvious to other software that the file is not a regular file, so this can be less dangerous
* hardlinks look like regular files, so other software may assume they are safe to edit without affecting the other copies
**warning:** if you edit the contents of a deduplicated file, then you will also edit all other copies of that file! This is especially surprising with hardlinks, because they look like regular files, but that same file exists in multiple locations
global-option `--xlink` / volflag `xlink` additionally enables deduplication across volumes, but this is probably buggy and not recommended
## file indexing
enables dedup and music search ++
enable music search, upload-undo, and better dedup
file indexing relies on two database tables, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`), stored in `.hist/up2k.db`. Configuration can be done through arguments, volflags, or a mix of both.
@@ -1168,10 +1220,9 @@ through arguments:
* `-e2t` enables metadata indexing on upload
* `-e2ts` also scans for tags in all files that don't have tags yet
* `-e2tsr` also deletes all existing tags, doing a full reindex
* `-e2v` verfies file integrity at startup, comparing hashes from the db
* `-e2v` verifies file integrity at startup, comparing hashes from the db
* `-e2vu` patches the database with the new hashes from the filesystem
* `-e2vp` panics and kills copyparty instead
* `--xlink` enables deduplication across volumes
the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling:
* `-v ~/music::r:c,e2ds,e2tsr` does a full reindex of everything on startup
@@ -1184,7 +1235,6 @@ note:
* upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings)
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
* deduplication is possible on windows if you run copyparty as administrator (not saying you should!)
### exclude-patterns
@@ -1383,11 +1433,27 @@ redefine behavior with plugins ([examples](./bin/handlers/))
replace 404 and 403 errors with something completely different (that's it for now)
## ip auth
autologin based on IP range (CIDR) , using the global-option `--ipu`
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, or put this in a config file:
```yaml
[global]
ipu: 192.168.123.0/24=spartacus
```
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=`
## identity providers
replace copyparty passwords with oauth and such
you can disable the built-in password-based login sysem, and instead replace it with a separate piece of software (an identity provider) which will then handle authenticating / authorizing of users; this makes it possible to login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions
you can disable the built-in password-based login system, and instead replace it with a separate piece of software (an identity provider) which will then handle authenticating / authorizing of users; this makes it possible to login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions
a popular choice is [Authelia](https://www.authelia.com/) (config-file based), another one is [authentik](https://goauthentik.io/) (GUI-based, more complex)
@@ -1414,7 +1480,7 @@ if permitted, users can change their own passwords in the control-panel
* if you run multiple copyparty instances with different users you *almost definitely* want to specify separate DBs for each instance
* if [password hashing](#password-hashing) is enbled, the passwords in the db are also hashed
* if [password hashing](#password-hashing) is enabled, the passwords in the db are also hashed
* ...which means that all user-defined passwords will be forgotten if you change password-hashing settings
@@ -1434,7 +1500,7 @@ you may improve performance by specifying larger values for `--iobuf` / `--s-rd-
## hiding from google
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
tell search engines you don't wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
* `--no-robots` adds HTTP (`X-Robots-Tag`) and HTML (`<meta>`) headers with `noindex, nofollow` globally
* volflag `[...]:c,norobots` does the same thing for that single volume
@@ -1509,6 +1575,33 @@ if you want to change the fonts, see [./docs/rice/](./docs/rice/)
`-lo log/cpp-%Y-%m%d-%H%M%S.txt.xz`
## listen on port 80 and 443
become a *real* webserver which people can access by just going to your IP or domain without specifying a port
**if you're on windows,** then you just need to add the commandline argument `-p 80,443` and you're done! nice
**if you're on macos,** sorry, I don't know
**if you're on Linux,** you have the following 4 options:
* **option 1:** set up a [reverse-proxy](#reverse-proxy) -- this one makes a lot of sense if you're running on a proper headless server, because that way you get real HTTPS too
* **option 2:** NAT to port 3923 -- this is cumbersome since you'll need to do it every time you reboot, and the exact command may depend on your linux distribution:
```bash
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3923
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 3923
```
* **option 3:** disable the [security policy](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html) which prevents the use of 80 and 443; this is *probably* fine:
```
setcap CAP_NET_BIND_SERVICE=+eip $(realpath $(which python))
python copyparty-sfx.py -p 80,443
```
* **option 4:** run copyparty as root (please don't)
## reverse-proxy
running copyparty next to other websites hosted on an existing webserver such as nginx, caddy, or apache
@@ -1518,6 +1611,8 @@ you can either:
* or do location-based proxying, using `--rp-loc=/stuff` to tell copyparty where it is mounted -- has a slight performance cost and higher chance of bugs
* if copyparty says `incorrect --rp-loc or webserver config; expected vpath starting with [...]` it's likely because the webserver is stripping away the proxy location from the request URLs -- see the `ProxyPass` in the apache example below
when running behind a reverse-proxy (this includes services like cloudflare), it is important to configure real-ip correctly, as many features rely on knowing the client's IP. Look out for red and yellow log messages which explain how to do this. But basically, set `--xff-hdr` to the name of the http header to read the IP from (usually `x-forwarded-for`, but cloudflare uses `cf-connecting-ip`), and then `--xff-src` to the IP of the reverse-proxy so copyparty will trust the xff-hdr. Note that `--rp-loc` in particular will not work at all unless you do this
some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which *could* be a nice speed boost, depending on a lot of factors
* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
@@ -1844,16 +1939,18 @@ interact with copyparty using non-browser clients
* FUSE: mount a copyparty server as a local filesystem
* cross-platform python client available in [./bin/](bin/)
* able to mount nginx and iis directory listings too, not just copyparty
* can be downloaded from copyparty: controlpanel -> connect -> [partyfuse.py](http://127.0.0.1:3923/.cpr/a/partyfuse.py)
* [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)
* sharex (screenshot utility): see [./contrib/sharex.sxcu](contrib/#sharexsxcu)
* and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)
* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
* [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`
copyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uplaods:
copyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uploads:
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
b512 <movie.mkv
@@ -1882,7 +1979,7 @@ alternatively, some alternatives roughly sorted by speed (unreproducible benchma
* [rclone-webdav](./docs/rclone.md) (25s), read/WRITE (rclone v1.63 or later)
* [rclone-http](./docs/rclone.md) (26s), read-only
* [partyfuse.py](./bin/#partyfusepy) (35s), read-only
* [partyfuse.py](./bin/#partyfusepy) (26s), read-only
* [rclone-ftp](./docs/rclone.md) (47s), read/WRITE
* davfs2 (103s), read/WRITE
* [win10-webdav](#webdav-server) (138s), read/WRITE
@@ -1924,6 +2021,9 @@ below are some tweaks roughly ordered by usefulness:
* `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file
* `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set
* and also makes thumbnails load faster, regardless of e2d/e2t
* `--dedup` enables deduplication and thus avoids writing to the HDD if someone uploads a dupe
* `--safe-dedup 1` makes deduplication much faster during upload by skipping verification of file contents; safe if there is no other software editing/moving the files in the volumes
* `--no-dirsz` shows the size of folder inodes instead of the total size of the contents, giving about 30% faster folder listings
* `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable
* if your volumes are on a network-disk such as NFS / SMB / s3, specifying larger values for `--iobuf` and/or `--s-rd-sz` and/or `--s-wr-sz` may help; try setting all of them to `524288` or `1048576` or `4194304`
* `--no-htp --hash-mt=0 --mtag-mt=1 --th-mt=1` minimizes the number of threads; can help in some eccentric environments (like the vscode debugger)
@@ -1950,7 +2050,7 @@ when uploading files,
* up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it)
* optionally you can switch to the lightweight potato ui by clicking the `[🥔]`
* switching to another browser-tab also works, the favicon will update every 10 seconds in that case
* unlikely to be a problem, but can happen when uploding many small files, or your internet is too fast, or PC too slow
* unlikely to be a problem, but can happen when uploading many small files, or your internet is too fast, or PC too slow
# security
@@ -1978,7 +2078,7 @@ safety profiles:
* `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance
* however note if you edit one file it will also affect the other copies
* `--vague-403` returns a "404 not found" instead of "401 unauthorized" which is a common enterprise meme
* `--nih` removes the server hostname from directory listings
* `-nih` removes the server hostname from directory listings
* option `-sss` is a shortcut for the above plus:
* `--no-dav` disables webdav support
@@ -1998,7 +2098,7 @@ other misc notes:
behavior that might be unexpected
* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example
* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `PREADME.md` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example
* users can submit `<script>`s which autorun (in a sandbox) for other visitors in a few ways;
* uploading a `README.md` -- avoid with `--no-readme`
* renaming `some.html` to `.epilogue.html` -- avoid with either `--no-logues` or `--no-dot-ren`
@@ -2041,6 +2141,8 @@ volflag `dky` disables the actual key-check, meaning anyone can see the contents
volflag `dks` lets people enter subfolders as well, and also enables download-as-zip/tar
if you enable dirkeys, it is probably a good idea to enable filekeys too, otherwise it will be impossible to hotlink files from a folder which was accessed using a dirkey
dirkeys are generated based on another salt (`--dk-salt`) + filesystem-path and have a few limitations:
* the key does not change if the contents of the folder is modified
* if you need a new dirkey, either change the salt or rename the folder
@@ -2074,13 +2176,13 @@ if [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, co
## client crashes
### frefox wsod
### firefox wsod
firefox 87 can crash during uploads -- the entire browser goes, including all other browser tabs, everything turns white
however you can hit `F12` in the up2k tab and use the devtools to see how far you got in the uploads:
* get a complete list of all uploads, organized by statuts (ok / no-good / busy / queued):
* get a complete list of all uploads, organized by status (ok / no-good / busy / queued):
`var tabs = { ok:[], ng:[], bz:[], q:[] }; for (var a of up2k.ui.tab) tabs[a.in].push(a); tabs`
* list of filenames which failed:
@@ -2123,7 +2225,7 @@ enable [thumbnails](#thumbnails) of...
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
enable [smb](#smb-server) support (**not** recommended):
* `impacket==0.11.0`
* `impacket==0.12.0`
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
@@ -2197,7 +2299,7 @@ then again, if you are already into downloading shady binaries from the internet
## zipapp
another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, is slow, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but it does not unpack any temporary files to disk, so it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
run it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails

View File

@@ -15,22 +15,18 @@ produces a chronological list of all uploads by collecting info from up2k databa
# [`partyfuse.py`](partyfuse.py)
* mount a copyparty server as a local filesystem (read-only)
* **supports Windows!** -- expect `194 MiB/s` sequential read
* **supports Linux** -- expect `117 MiB/s` sequential read
* **supports Linux** -- expect `600 MiB/s` sequential read
* **supports macos** -- expect `85 MiB/s` sequential read
filecache is default-on for windows and macos;
* macos readsize is 64kB, so speed ~32 MiB/s without the cache
* windows readsize varies by software; explorer=1M, pv=32k
note that copyparty should run with `-ed` to enable dotfiles (hidden otherwise)
also consider using [../docs/rclone.md](../docs/rclone.md) instead for 5x performance
and consider using [../docs/rclone.md](../docs/rclone.md) instead; usually a bit faster, especially on windows
## to run this on windows:
* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
* [x] add python 3.x to PATH (it asks during install)
* `python -m pip install --user fusepy`
* `python -m pip install --user fusepy` (or grab a copy of `fuse.py` from the `connect` page on your copyparty, and keep it in the same folder)
* `python ./partyfuse.py n: http://192.168.1.69:3923/`
10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,35 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "1.23"
S_BUILD_DT = "2024-08-22"
S_VERSION = "2.2"
S_BUILD_DT = "2024-10-13"
"""
u2c.py: upload to copyparty
2021, ed <irc.rizon.net>, MIT-Licensed
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
- dependencies: requests
- dependencies: no
- supports python 2.6, 2.7, and 3.3 through 3.12
- if something breaks just try again and it'll autoresume
"""
import re
import os
import sys
import stat
import math
import time
import json
import atexit
import base64
import binascii
import datetime
import hashlib
import json
import math
import os
import platform
import re
import signal
import socket
import base64
import hashlib
import platform
import stat
import sys
import threading
import datetime
import time
EXE = bool(getattr(sys, "frozen", False))
@@ -39,26 +40,12 @@ except:
print(m)
raise
try:
import requests
except ImportError as ex:
if EXE:
raise
elif sys.version_info > (2, 7):
m = "\nERROR: need 'requests'; please run this command:\n {0} -m pip install --user requests\n"
else:
m = "requests/2.18.4 urllib3/1.23 chardet/3.0.4 certifi/2020.4.5.1 idna/2.7"
m = [" https://pypi.org/project/" + x + "/#files" for x in m.split()]
m = "\n ERROR: need these:\n" + "\n".join(m) + "\n"
m += "\n for f in *.whl; do unzip $f; done; rm -r *.dist-info\n"
print(m.format(sys.executable), "\nspecifically,", ex)
sys.exit(1)
# from copyparty/__init__.py
PY2 = sys.version_info < (3,)
PY27 = sys.version_info > (2, 7) and PY2
PY37 = sys.version_info > (3, 7)
if PY2:
import httplib as http_client
from Queue import Queue
from urllib import quote, unquote
from urlparse import urlsplit, urlunsplit
@@ -66,17 +53,50 @@ if PY2:
sys.dont_write_bytecode = True
bytes = str
else:
from queue import Queue
from urllib.parse import unquote_to_bytes as unquote
from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote
from urllib.parse import urlsplit, urlunsplit
import http.client as http_client
from queue import Queue
unicode = str
VT100 = platform.system() != "Windows"
req_ses = requests.Session()
try:
UTC = datetime.timezone.utc
except:
TD_ZERO = datetime.timedelta(0)
class _UTC(datetime.tzinfo):
def utcoffset(self, dt):
return TD_ZERO
def tzname(self, dt):
return "UTC"
def dst(self, dt):
return TD_ZERO
UTC = _UTC()
try:
_b64etl = bytes.maketrans(b"+/", b"-_")
def ub64enc(bs):
x = binascii.b2a_base64(bs, newline=False)
return x.translate(_b64etl)
ub64enc(b"a")
except:
ub64enc = base64.urlsafe_b64encode
class BadAuth(Exception):
pass
class Daemon(threading.Thread):
@@ -96,6 +116,108 @@ class Daemon(threading.Thread):
self.fun(*self.a)
class HSQueue(Queue):
def _init(self, maxsize):
from collections import deque
self.q = deque()
def _qsize(self):
return len(self.q)
def _put(self, item):
if item and item.nhs:
self.q.appendleft(item)
else:
self.q.append(item)
def _get(self):
return self.q.popleft()
class HCli(object):
def __init__(self, ar):
self.ar = ar
url = urlsplit(ar.url)
tls = url.scheme.lower() == "https"
try:
addr, port = url.netloc.split(":")
except:
addr = url.netloc
port = 443 if tls else 80
self.addr = addr
self.port = int(port)
self.tls = tls
self.verify = ar.te or not ar.td
self.conns = []
if tls:
import ssl
if not self.verify:
self.ctx = ssl._create_unverified_context()
elif self.verify is True:
self.ctx = None
else:
self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
self.ctx.load_verify_locations(self.verify)
self.base_hdrs = {
"Accept": "*/*",
"Connection": "keep-alive",
"Host": url.netloc,
"Origin": self.ar.burl,
"User-Agent": "u2c/%s" % (S_VERSION,),
}
def _connect(self):
args = {}
if PY37:
args["blocksize"] = 1048576
if not self.tls:
C = http_client.HTTPConnection
else:
C = http_client.HTTPSConnection
if self.ctx:
args = {"context": self.ctx}
return C(self.addr, self.port, timeout=999, **args)
def req(self, meth, vpath, hdrs, body=None, ctype=None):
hdrs.update(self.base_hdrs)
if self.ar.a:
hdrs["PW"] = self.ar.a
if ctype:
hdrs["Content-Type"] = ctype
if meth == "POST" and CLEN not in hdrs:
hdrs[CLEN] = (
0 if not body else body.len if hasattr(body, "len") else len(body)
)
c = self.conns.pop() if self.conns else self._connect()
try:
c.request(meth, vpath, body, hdrs)
if PY27:
rsp = c.getresponse(buffering=True)
else:
rsp = c.getresponse()
data = rsp.read()
self.conns.append(c)
return rsp.status, data.decode("utf-8")
except:
c.close()
raise
MJ = "application/json"
MO = "application/octet-stream"
CLEN = "Content-Length"
web = None # type: HCli
class File(object):
"""an up2k upload task; represents a single file"""
@@ -128,9 +250,6 @@ class File(object):
self.up_c = 0 # type: int
self.cd = 0 # type: int
# t = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n"
# eprint(t.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name))
class FileSlice(object):
"""file-like object providing a fixed window into a file"""
@@ -263,14 +382,19 @@ class MTHash(object):
chunk_rem -= len(buf)
ofs += len(buf)
digest = hashobj.digest()[:33]
digest = base64.urlsafe_b64encode(digest).decode("utf-8")
digest = ub64enc(hashobj.digest()[:33]).decode("utf-8")
return nch, digest, ofs0, chunk_sz
_print = print
def safe_print(*a, **ka):
ka["end"] = ""
zs = " ".join([unicode(x) for x in a])
_print(zs + "\n", **ka)
def eprint(*a, **ka):
ka["file"] = sys.stderr
ka["end"] = ""
@@ -284,18 +408,17 @@ def eprint(*a, **ka):
def flushing_print(*a, **ka):
try:
_print(*a, **ka)
safe_print(*a, **ka)
except:
v = " ".join(str(x) for x in a)
v = v.encode("ascii", "replace").decode("ascii")
_print(v, **ka)
safe_print(v, **ka)
if "flush" not in ka:
sys.stdout.flush()
if not VT100:
print = flushing_print
print = safe_print if VT100 else flushing_print
def termsize():
@@ -303,7 +426,9 @@ def termsize():
def ioctl_GWINSZ(fd):
try:
import fcntl, termios, struct
import fcntl
import struct
import termios
r = struct.unpack(b"hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, b"AAAA"))
return r[::-1]
@@ -361,8 +486,8 @@ class CTermsize(object):
eprint("\033[s\033[r\033[u")
else:
self.g = 1 + self.h - margin
t = "{0}\033[{1}A".format("\n" * margin, margin)
eprint("{0}\033[s\033[1;{1}r\033[u".format(t, self.g - 1))
t = "%s\033[%dA" % ("\n" * margin, margin)
eprint("%s\033[s\033[1;%dr\033[u" % (t, self.g - 1))
ss = CTermsize()
@@ -379,14 +504,14 @@ def undns(url):
except KeyboardInterrupt:
raise
except:
t = "\n\033[31mfailed to resolve upload destination host;\033[0m\ngai={0}\n"
eprint(t.format(repr(gai)))
t = "\n\033[31mfailed to resolve upload destination host;\033[0m\ngai=%r\n"
eprint(t % (gai,))
raise
if usp.port:
hn = "{0}:{1}".format(hn, usp.port)
hn = "%s:%s" % (hn, usp.port)
if usp.username or usp.password:
hn = "{0}:{1}@{2}".format(usp.username, usp.password, hn)
hn = "%s:%s@%s" % (usp.username, usp.password, hn)
usp = usp._replace(netloc=hn)
url = urlunsplit(usp)
@@ -421,7 +546,7 @@ else:
statdir = _lsd
def walkdir(err, top, seen):
def walkdir(err, top, excl, seen):
"""recursive statdir"""
atop = os.path.abspath(os.path.realpath(top))
if atop in seen:
@@ -430,10 +555,12 @@ def walkdir(err, top, seen):
seen = seen[:] + [atop]
for ap, inf in sorted(statdir(err, top)):
if excl.match(ap):
continue
yield ap, inf
if stat.S_ISDIR(inf.st_mode):
try:
for x in walkdir(err, ap, seen):
for x in walkdir(err, ap, excl, seen):
yield x
except Exception as ex:
err.append((ap, str(ex)))
@@ -473,9 +600,7 @@ def walkdirs(err, tops, excl):
yield stop, dn, os.stat(stop)
if isdir:
for ap, inf in walkdir(err, top, []):
if ptn.match(ap):
continue
for ap, inf in walkdir(err, top, ptn, []):
yield stop, ap[len(stop) :].lstrip(sep), inf
else:
d, n = top.rsplit(sep, 1)
@@ -551,8 +676,7 @@ def get_hashlist(file, pcb, mth):
hashobj.update(buf)
chunk_rem -= len(buf)
digest = hashobj.digest()[:33]
digest = base64.urlsafe_b64encode(digest).decode("utf-8")
digest = ub64enc(hashobj.digest()[:33]).decode("utf-8")
ret.append([digest, file_ofs, chunk_sz])
file_ofs += chunk_sz
@@ -577,9 +701,6 @@ def handshake(ar, file, search):
otherwise, a list of chunks to upload
"""
url = ar.url
pw = ar.a
req = {
"hash": [x[0] for x in file.cids],
"name": file.name,
@@ -594,28 +715,27 @@ def handshake(ar, file, search):
if ar.ow:
req["replace"] = True
headers = {"Content-Type": "text/plain"} # <=1.5.1 compat
if pw:
headers["Cookie"] = "=".join(["cppwd", pw])
file.recheck = False
if file.url:
url = file.url
elif b"/" in file.rel:
url += quotep(file.rel.rsplit(b"/", 1)[0]).decode("utf-8", "replace")
else:
if b"/" in file.rel:
url = quotep(file.rel.rsplit(b"/", 1)[0]).decode("utf-8", "replace")
else:
url = ""
url = ar.vtop + url
while True:
sc = 600
txt = ""
t0 = time.time()
try:
zs = json.dumps(req, separators=(",\n", ": "))
r = req_ses.post(url, headers=headers, data=zs)
sc = r.status_code
txt = r.text
sc, txt = web.req("POST", url, {}, zs.encode("utf-8"), MJ)
if sc < 400:
break
raise Exception("http {0}: {1}".format(sc, txt))
raise Exception("http %d: %s" % (sc, txt))
except Exception as ex:
em = str(ex).split("SSLError(")[-1].split("\nURL: ")[0].strip()
@@ -629,35 +749,32 @@ def handshake(ar, file, search):
return [], False
elif sc == 409 or "<pre>upload rejected, file already exists" in txt:
return [], False
elif "<pre>you don't have " in txt:
raise
elif sc == 403:
print("\nERROR: login required, or wrong password:\n%s" % (txt,))
raise BadAuth()
eprint("handshake failed, retrying: {0}\n {1}\n\n".format(file.name, em))
t = "handshake failed, retrying: %s\n t0=%.3f t1=%.3f td=%.3f\n %s\n\n"
now = time.time()
eprint(t % (file.name, t0, now, now - t0, em))
time.sleep(ar.cd)
try:
r = r.json()
r = json.loads(txt)
except:
raise Exception(r.text)
raise Exception(txt)
if search:
return r["hits"], False
try:
pre, url = url.split("://")
pre += "://"
except:
pre = ""
file.url = pre + url.split("/")[0] + r["purl"]
file.url = r["purl"]
file.name = r["name"]
file.wark = r["wark"]
return r["hash"], r["sprs"]
def upload(fsl, pw, stats):
# type: (FileSlice, str, str) -> None
def upload(fsl, stats):
# type: (FileSlice, str) -> None
"""upload a range of file data, defined by one or more `cid` (chunk-hash)"""
ctxt = fsl.cids[0]
@@ -670,20 +787,15 @@ def upload(fsl, pw, stats):
headers = {
"X-Up2k-Hash": ctxt,
"X-Up2k-Wark": fsl.file.wark,
"Content-Type": "application/octet-stream",
}
if stats:
headers["X-Up2k-Stat"] = stats
if pw:
headers["Cookie"] = "=".join(["cppwd", pw])
try:
r = req_ses.post(fsl.file.url, headers=headers, data=fsl)
sc, txt = web.req("POST", fsl.file.url, headers, fsl, MO)
if r.status_code == 400:
txt = r.text
if sc == 400:
if (
"already being written" in txt
or "already got that" in txt
@@ -691,10 +803,8 @@ def upload(fsl, pw, stats):
):
fsl.file.nojoin = 1
if not r:
raise Exception(repr(r))
_ = r.content
if sc >= 400:
raise Exception("http %s: %s" % (sc, txt))
finally:
fsl.f.close()
@@ -707,7 +817,7 @@ class Ctl(object):
def _scan(self):
ar = self.ar
eprint("\nscanning {0} locations\n".format(len(ar.files)))
eprint("\nscanning %d locations\n" % (len(ar.files),))
nfiles = 0
nbytes = 0
err = []
@@ -719,14 +829,14 @@ class Ctl(object):
nbytes += inf.st_size
if err:
eprint("\n# failed to access {0} paths:\n".format(len(err)))
eprint("\n# failed to access %d paths:\n" % (len(err),))
for ap, msg in err:
if ar.v:
eprint("{0}\n `-{1}\n\n".format(ap.decode("utf-8", "replace"), msg))
eprint("%s\n `-%s\n\n" % (ap.decode("utf-8", "replace"), msg))
else:
eprint(ap.decode("utf-8", "replace") + "\n")
eprint("^ failed to access those {0} paths ^\n\n".format(len(err)))
eprint("^ failed to access those %d paths ^\n\n" % (len(err),))
if not ar.v:
eprint("hint: set -v for detailed error messages\n")
@@ -735,11 +845,12 @@ class Ctl(object):
eprint("hint: aborting because --ok is not set\n")
return
eprint("found {0} files, {1}\n\n".format(nfiles, humansize(nbytes)))
eprint("found %d files, %s\n\n" % (nfiles, humansize(nbytes)))
return nfiles, nbytes
def __init__(self, ar, stats=None):
self.ok = False
self.panik = 0
self.errs = 0
self.ar = ar
self.stats = stats or self._scan()
@@ -747,13 +858,6 @@ class Ctl(object):
return
self.nfiles, self.nbytes = self.stats
if ar.td:
requests.packages.urllib3.disable_warnings()
req_ses.verify = False
if ar.te:
req_ses.verify = ar.te
self.filegen = walkdirs([], ar.files, ar.x)
self.recheck = [] # type: list[File]
@@ -768,10 +872,8 @@ class Ctl(object):
self.hash_b = 0
self.up_f = 0
self.up_c = 0
self.up_b = 0
self.up_br = 0
self.hasher_busy = 1
self.handshaker_busy = 0
self.up_b = 0 # num bytes handled
self.up_br = 0 # num bytes actually transferred
self.uploader_busy = 0
self.serialized = False
@@ -781,7 +883,10 @@ class Ctl(object):
self.eta = "99:99:99"
self.mutex = threading.Lock()
self.q_handshake = Queue() # type: Queue[File]
self.exit_cond = threading.Condition()
self.uploader_alive = ar.j
self.handshaker_alive = ar.j
self.q_handshake = HSQueue() # type: Queue[File]
self.q_upload = Queue() # type: Queue[FileSlice]
self.st_hash = [None, "(idle, starting...)"] # type: tuple[File, int]
@@ -796,24 +901,29 @@ class Ctl(object):
def _safe(self):
"""minimal basic slow boring fallback codepath"""
search = self.ar.s
for nf, (top, rel, inf) in enumerate(self.filegen):
nf = 0
for top, rel, inf in self.filegen:
if stat.S_ISDIR(inf.st_mode) or not rel:
continue
nf += 1
file = File(top, rel, inf.st_size, inf.st_mtime)
upath = file.abs.decode("utf-8", "replace")
print("{0} {1}\n hash...".format(self.nfiles - nf, upath))
print("%d %s\n hash..." % (self.nfiles - nf, upath))
get_hashlist(file, None, None)
burl = self.ar.url[:12] + self.ar.url[8:].split("/")[0] + "/"
while True:
print(" hs...")
hs, _ = handshake(self.ar, file, search)
try:
hs, _ = handshake(self.ar, file, search)
except BadAuth:
sys.exit(1)
if search:
if hs:
for hit in hs:
print(" found: {0}{1}".format(burl, hit["rp"]))
print(" found: %s/%s" % (self.ar.burl, hit["rp"]))
else:
print(" NOT found")
break
@@ -822,13 +932,13 @@ class Ctl(object):
if not hs:
break
print("{0} {1}".format(self.nfiles - nf, upath))
print("%d %s" % (self.nfiles - nf, upath))
ncs = len(hs)
for nc, cid in enumerate(hs):
print(" {0} up {1}".format(ncs - nc, cid))
stats = "{0}/0/0/{1}".format(nf, self.nfiles - nf)
print(" %d up %s" % (ncs - nc, cid))
stats = "%d/0/0/%d" % (nf, self.nfiles - nf)
fslice = FileSlice(file, [cid])
upload(fslice, self.ar.a, stats)
upload(fslice, stats)
print(" ok!")
if file.recheck:
@@ -839,7 +949,7 @@ class Ctl(object):
eprint("finalizing %d duplicate files\n" % (len(self.recheck),))
for file in self.recheck:
handshake(self.ar, file, search)
handshake(self.ar, file, False)
def _fancy(self):
if VT100 and not self.ar.ns:
@@ -851,32 +961,28 @@ class Ctl(object):
Daemon(self.handshaker)
Daemon(self.uploader)
idles = 0
while idles < 3:
time.sleep(0.07)
while True:
with self.exit_cond:
self.exit_cond.wait(0.07)
if self.panik:
sys.exit(1)
with self.mutex:
if (
self.q_handshake.empty()
and self.q_upload.empty()
and not self.hasher_busy
and not self.handshaker_busy
and not self.uploader_busy
):
idles += 1
else:
idles = 0
if not self.handshaker_alive and not self.uploader_alive:
break
st_hash = self.st_hash[:]
st_up = self.st_up[:]
if VT100 and not self.ar.ns:
maxlen = ss.w - len(str(self.nfiles)) - 14
txt = "\033[s\033[{0}H".format(ss.g)
txt = "\033[s\033[%dH" % (ss.g,)
for y, k, st, f in [
[0, "hash", self.st_hash, self.hash_f],
[1, "send", self.st_up, self.up_f],
[0, "hash", st_hash, self.hash_f],
[1, "send", st_up, self.up_f],
]:
txt += "\033[{0}H{1}:".format(ss.g + y, k)
txt += "\033[%dH%s:" % (ss.g + y, k)
file, arg = st
if not file:
txt += " {0}\033[K".format(arg)
txt += " %s\033[K" % (arg,)
else:
if y:
p = 100 * file.up_b / file.size
@@ -885,12 +991,11 @@ class Ctl(object):
name = file.abs.decode("utf-8", "replace")[-maxlen:]
if "/" in name:
name = "\033[36m{0}\033[0m/{1}".format(*name.rsplit("/", 1))
name = "\033[36m%s\033[0m/%s" % tuple(name.rsplit("/", 1))
t = "{0:6.1f}% {1} {2}\033[K"
txt += t.format(p, self.nfiles - f, name)
txt += "%6.1f%% %d %s\033[K" % (p, self.nfiles - f, name)
txt += "\033[{0}H ".format(ss.g + 2)
txt += "\033[%dH " % (ss.g + 2,)
else:
txt = " "
@@ -908,14 +1013,17 @@ class Ctl(object):
nleft = self.nfiles - self.up_f
tail = "\033[K\033[u" if VT100 and not self.ar.ns else "\r"
t = "{0} eta @ {1}/s, {2}, {3}# left".format(self.eta, spd, sleft, nleft)
t = "%s eta @ %s/s, %s, %d# left\033[K" % (self.eta, spd, sleft, nleft)
eprint(txt + "\033]0;{0}\033\\\r{0}{1}".format(t, tail))
if self.ar.wlist:
self.at_hash = time.time() - self.t0
if self.hash_b and self.at_hash:
spd = humansize(self.hash_b / self.at_hash)
eprint("\nhasher: %.2f sec, %s/s\n" % (self.at_hash, spd))
if self.up_b and self.at_up:
spd = humansize(self.up_b / self.at_up)
if self.up_br and self.at_up:
spd = humansize(self.up_br / self.at_up)
eprint("upload: %.2f sec, %s/s\n" % (self.at_up, spd))
if not self.recheck:
@@ -944,20 +1052,18 @@ class Ctl(object):
srd = rd.decode("utf-8", "replace").replace("\\", "/")
if prd != rd:
prd = rd
headers = {}
if self.ar.a:
headers["Cookie"] = "=".join(["cppwd", self.ar.a])
ls = {}
try:
print(" ls ~{0}".format(srd))
zb = self.ar.url.encode("utf-8")
zb += quotep(rd.replace(b"\\", b"/"))
r = req_ses.get(zb + b"?ls&lt&dots", headers=headers)
if not r:
raise Exception("HTTP {0}".format(r.status_code))
zt = (
self.ar.vtop,
quotep(rd.replace(b"\\", b"/")).decode("utf-8", "replace"),
)
sc, txt = web.req("GET", "%s%s?ls&lt&dots" % zt, {})
if sc >= 400:
raise Exception("http %s" % (sc,))
j = r.json()
j = json.loads(txt)
for f in j["dirs"] + j["files"]:
rfn = f["href"].split("?")[0].rstrip("/")
ls[unquote(rfn.encode("utf-8", "replace"))] = f
@@ -980,14 +1086,17 @@ class Ctl(object):
req = locs
while req:
print("DELETING ~%s/#%s" % (srd, len(req)))
r = req_ses.post(self.ar.url + "?delete", json=req)
if r.status_code == 413 and "json 2big" in r.text:
body = json.dumps(req).encode("utf-8")
sc, txt = web.req(
"POST", self.ar.url + "?delete", {}, body, MJ
)
if sc == 413 and "json 2big" in txt:
print(" (delete request too big; slicing...)")
req = req[: len(req) // 2]
continue
elif not r:
t = "delete request failed: %r %s"
raise Exception(t % (r, r.text))
elif sc >= 400:
t = "delete request failed: %s %s"
raise Exception(t % (sc, txt))
break
locs = locs[len(req) :]
@@ -1027,21 +1136,60 @@ class Ctl(object):
self.hash_f += 1
self.hash_c += len(file.cids)
self.hash_b += file.size
if self.ar.wlist:
self.up_f = self.hash_f
self.up_c = self.hash_c
self.up_b = self.hash_b
if self.ar.wlist:
vp = file.rel.decode("utf-8")
if self.ar.chs:
zsl = [
"%s %d %d" % (zsii[0], n, zsii[1])
for n, zsii in enumerate(file.cids)
]
print("chs: %s\n%s" % (vp, "\n".join(zsl)))
zsl = [self.ar.wsalt, str(file.size)] + [x[0] for x in file.kchunks]
zb = hashlib.sha512("\n".join(zsl).encode("utf-8")).digest()[:33]
wark = ub64enc(zb).decode("utf-8")
if self.ar.jw:
print("%s %s" % (wark, vp))
else:
zd = datetime.datetime.fromtimestamp(file.lmod, UTC)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year,
zd.month,
zd.day,
zd.hour,
zd.minute,
zd.second,
)
print("%s %12d %s %s" % (dt, file.size, wark, vp))
continue
self.q_handshake.put(file)
self.hasher_busy = 0
self.st_hash = [None, "(finished)"]
self._check_if_done()
def _check_if_done(self):
with self.mutex:
if self.nfiles - self.up_f:
return
for _ in range(self.ar.j):
self.q_handshake.put(None)
def handshaker(self):
search = self.ar.s
burl = self.ar.url[:8] + self.ar.url[8:].split("/")[0] + "/"
while True:
file = self.q_handshake.get()
if not file:
with self.mutex:
self.handshaker_alive -= 1
self.q_upload.put(None)
break
return
chunksz = up2k_chunksize(file.size)
upath = file.abs.decode("utf-8", "replace")
if not VT100:
upath = upath.lstrip("\\?")
@@ -1052,27 +1200,28 @@ class Ctl(object):
self.errs += 1
continue
with self.mutex:
self.handshaker_busy += 1
while time.time() < file.cd:
time.sleep(0.1)
hs, sprs = handshake(self.ar, file, search)
try:
hs, sprs = handshake(self.ar, file, search)
except BadAuth:
self.panik = 1
break
if search:
if hs:
for hit in hs:
t = "found: {0}\n {1}{2}\n"
print(t.format(upath, burl, hit["rp"]), end="")
print("found: %s\n %s/%s" % (upath, self.ar.burl, hit["rp"]))
else:
print("NOT found: {0}\n".format(upath), end="")
print("NOT found: {0}".format(upath))
with self.mutex:
self.up_f += 1
self.up_c += len(file.cids)
self.up_b += file.size
self.handshaker_busy -= 1
self._check_if_done()
continue
if file.recheck:
@@ -1100,11 +1249,15 @@ class Ctl(object):
file.up_c -= len(hs)
for cid in hs:
sz = file.kchunks[cid][1]
self.up_br -= sz
self.up_b -= sz
file.up_b -= sz
if hs and not file.up_b:
# first hs of this file; is this an upload resume?
file.up_b = chunksz * max(0, len(file.kchunks) - len(hs))
file.ucids = hs
self.handshaker_busy -= 1
if not hs:
self.at_hash += file.t_hash
@@ -1117,7 +1270,7 @@ class Ctl(object):
c1 = c2 = ""
spd_h = humansize(file.size / file.t_hash, True)
if file.up_b:
if file.up_c:
t_up = file.t1_up - file.t0_up
spd_u = humansize(file.size / t_up, True)
@@ -1127,10 +1280,12 @@ class Ctl(object):
t = " found %s %s(%.2fs,%s/s)%s"
print(t % (upath, c1, file.t_hash, spd_h, c2))
else:
kw = "uploaded" if file.up_b else " found"
kw = "uploaded" if file.up_c else " found"
print("{0} {1}".format(kw, upath))
chunksz = up2k_chunksize(file.size)
self._check_if_done()
continue
njoin = (self.ar.sz * 1024 * 1024) // chunksz
cs = hs[:]
while cs:
@@ -1149,8 +1304,16 @@ class Ctl(object):
while True:
fsl = self.q_upload.get()
if not fsl:
self.st_up = [None, "(finished)"]
break
done = False
with self.mutex:
self.uploader_alive -= 1
if not self.uploader_alive:
done = not self.handshaker_alive
self.st_up = [None, "(finished)"]
if done:
with self.exit_cond:
self.exit_cond.notify_all()
return
file = fsl.file
cids = fsl.cids
@@ -1175,7 +1338,7 @@ class Ctl(object):
)
try:
upload(fsl, self.ar.a, stats)
upload(fsl, stats)
except Exception as ex:
t = "upload failed, retrying: %s #%s+%d (%s)\n"
eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))
@@ -1209,7 +1372,10 @@ class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFor
def main():
global web
time.strptime("19970815", "%Y%m%d") # python#7980
"".encode("idna") # python#29288
if not VT100:
os.system("rem") # enables colors
@@ -1224,7 +1390,7 @@ def main():
sys.argv = [x for x in sys.argv if x != "--ws"]
# fmt: off
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool, " + ver, epilog="""
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:
"foo" uploads the entire folder to URL/foo/
@@ -1252,6 +1418,11 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("--dr", action="store_true", help="delete remote files which don't exist locally (implies --ow)")
ap.add_argument("--drd", action="store_true", help="delete remote files during upload instead of afterwards; reduces peak disk space usage, but will reupload instead of detecting renames")
ap = app.add_argument_group("file-ID calculator; enable with url '-' to list warks (file identifiers) instead of upload/search")
ap.add_argument("--wsalt", type=unicode, metavar="S", default="hunter2", help="salt to use when creating warks; must match server config")
ap.add_argument("--chs", action="store_true", help="verbose (print the hash/offset of each chunk in each file)")
ap.add_argument("--jw", action="store_true", help="just identifier+filepath, not mtime/size too")
ap = app.add_argument_group("performance tweaks")
ap.add_argument("-j", type=int, metavar="CONNS", default=2, help="parallel connections")
ap.add_argument("-J", type=int, metavar="CORES", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
@@ -1285,7 +1456,9 @@ source file/folder selection uses rsync syntax, meaning that:
ar.x = "|".join(ar.x or [])
for k in "dl dr drd".split():
setattr(ar, "wlist", ar.url == "-")
for k in "dl dr drd wlist".split():
errs = []
if ar.safe and getattr(ar, k):
errs.append(k)
@@ -1299,13 +1472,20 @@ source file/folder selection uses rsync syntax, meaning that:
for x in ar.files
]
ar.url = ar.url.rstrip("/") + "/"
if "://" not in ar.url:
ar.url = "http://" + ar.url
# urlsplit needs scheme;
zs = ar.url.rstrip("/") + "/"
if "://" not in zs:
zs = "http://" + zs
ar.url = zs
url = urlsplit(zs)
ar.burl = "%s://%s" % (url.scheme, url.netloc)
ar.vtop = url.path
if "https://" in ar.url.lower():
try:
import ssl, zipfile
import ssl
import zipfile
except:
t = "ERROR: https is not available for some reason; please use http"
print("\n\n %s\n\n" % (t,))
@@ -1330,6 +1510,7 @@ source file/folder selection uses rsync syntax, meaning that:
if ar.cls:
eprint("\033[H\033[2J\033[3J", end="")
web = HCli(ar)
ctl = Ctl(ar)
if ar.dr and not ar.drd and ctl.ok:

View File

@@ -19,6 +19,9 @@
* the `act:bput` thing is optional since copyparty v1.9.29
* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu)
### [`flameshot.sh`](flameshot.sh)
* takes a screenshot with [flameshot](https://flameshot.org/) on Linux, uploads it, and writes the URL to clipboard
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
* browser integration, kind of? custom rightclick actions and stuff
* rightclick a pic and send it to copyparty straight from your browser

14
contrib/flameshot.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -e
# take a screenshot with flameshot and send it to copyparty;
# the image url will be placed on your clipboard
password=wark
url=https://a.ocv.me/up/
filename=$(date +%Y-%m%d-%H%M%S).png
flameshot gui -s -r |
curl -T- $url$filename?pw=$password |
tail -n 1 |
xsel -ib

View File

@@ -1,14 +1,10 @@
# when running copyparty behind a reverse proxy,
# the following arguments are recommended:
#
# -i 127.0.0.1 only accept connections from nginx
#
# -nc must match or exceed the webserver's max number of concurrent clients;
# copyparty default is 1024 if OS permits it (see "max clients:" on startup),
# look for "max clients:" when starting copyparty, as nginx should
# not accept more consecutive clients than what copyparty is able to;
# nginx default is 512 (worker_processes 1, worker_connections 512)
#
# you may also consider adding -j0 for CPU-intensive configurations
# (5'000 requests per second, or 20gbps upload/download in parallel)
# rarely, in some extreme usecases, it can be good to add -j0
# (40'000 requests per second, or 20gbps upload/download in parallel)
# but this is usually counterproductive and slightly buggy
#
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
#
@@ -20,10 +16,33 @@
#
# and then enable it below by uncomenting the cloudflare-only.conf line
upstream cpp {
upstream cpp_tcp {
# alternative 1: connect to copyparty using tcp;
# cpp_uds is slightly faster and more secure, but
# cpp_tcp is easier to setup and "just works"
# ...you should however restrict copyparty to only
# accept connections from nginx by adding these args:
# -i 127.0.0.1
server 127.0.0.1:3923 fail_timeout=1s;
keepalive 1;
}
upstream cpp_uds {
# alternative 2: unix-socket, aka. "unix domain socket";
# 5-10% faster, and better isolation from other software,
# but there must be at least one unix-group which both
# nginx and copyparty is a member of; if that group is
# "www" then run copyparty with the following args:
# -i unix:770:www:/tmp/party.sock
server unix:/tmp/party.sock fail_timeout=1s;
keepalive 1;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
@@ -34,7 +53,8 @@ server {
#include /etc/nginx/cloudflare-only.conf;
location / {
proxy_pass http://cpp;
# recommendation: replace cpp_tcp with cpp_uds below
proxy_pass http://cpp_tcp;
proxy_redirect off;
# disable buffering (next 4 lines)
proxy_http_version 1.1;
@@ -52,6 +72,7 @@ server {
}
}
# default client_max_body_size (1M) blocks uploads larger than 256 MiB
client_max_body_size 1024M;
client_header_timeout 610m;

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty
pkgver="1.14.2"
pkgver="1.15.6"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" )
sha256sums=("a39f3950c663671d635c453d1a400f6cec6ec827e7dc9d22c3e791b8ab54017b")
sha256sums=("abb5c1705cd80ea553d647d4a7b35b5e1dac5a517200551bcca79aa199f30875")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.14.2/copyparty-sfx.py",
"version": "1.14.2",
"hash": "sha256-n9Dj2MMrvkWhlXAKWOXn5YQsFCxNpgo5HDFQ111a66A="
"url": "https://github.com/9001/copyparty/releases/download/v1.15.6/copyparty-sfx.py",
"version": "1.15.6",
"hash": "sha256-0ikt3jv9/XT/w/ew+R4rZxF6s7LwNhUvUYYIZtkQqbk="
}

View File

@@ -16,9 +16,10 @@ except:
TYPE_CHECKING = False
if True:
from typing import Any, Callable
from typing import Any, Callable, Optional
PY2 = sys.version_info < (3,)
PY36 = sys.version_info > (3, 6)
if not PY2:
unicode: Callable[[Any], str] = str
else:
@@ -50,6 +51,60 @@ try:
except:
CORES = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2
# all embedded resources to be retrievable over http
zs = """
web/a/partyfuse.py
web/a/u2c.py
web/a/webdav-cfg.bat
web/baguettebox.js
web/browser.css
web/browser.html
web/browser.js
web/browser2.html
web/cf.html
web/copyparty.gif
web/dd/2.png
web/dd/3.png
web/dd/4.png
web/dd/5.png
web/deps/busy.mp3
web/deps/easymde.css
web/deps/easymde.js
web/deps/marked.js
web/deps/fuse.py
web/deps/mini-fa.css
web/deps/mini-fa.woff
web/deps/prism.css
web/deps/prism.js
web/deps/prismd.css
web/deps/scp.woff2
web/deps/sha512.ac.js
web/deps/sha512.hw.js
web/md.css
web/md.html
web/md.js
web/md2.css
web/md2.js
web/mde.css
web/mde.html
web/mde.js
web/msg.css
web/msg.html
web/shares.css
web/shares.html
web/shares.js
web/splash.css
web/splash.html
web/splash.js
web/svcs.html
web/svcs.js
web/ui.css
web/up2k.js
web/util.js
web/w.hash.js
"""
RES = set(zs.strip().split("\n"))
class EnvParams(object):
def __init__(self) -> None:

View File

@@ -27,6 +27,7 @@ from .__init__ import (
EXE,
MACOS,
PY2,
PY36,
VT100,
WINDOWS,
E,
@@ -54,7 +55,10 @@ from .util import (
Daemon,
align_tab,
ansi_re,
b64enc,
dedent,
has_resource,
load_resource,
min_ex,
pybin,
termsize,
@@ -204,7 +208,7 @@ def init_E(EE: EnvParams) -> None:
errs.append("Using [%s] instead" % (p,))
if errs:
print("WARNING: " + ". ".join(errs))
warn(". ".join(errs))
return p # type: ignore
except Exception as ex:
@@ -234,7 +238,7 @@ def init_E(EE: EnvParams) -> None:
raise
def get_srvname() -> str:
def get_srvname(verbose) -> str:
try:
ret: str = unicode(socket.gethostname()).split(".")[0]
except:
@@ -244,7 +248,8 @@ def get_srvname() -> str:
return ret
fp = os.path.join(E.cfg, "name.txt")
lprint("using hostname from {}\n".format(fp))
if verbose:
lprint("using hostname from {}\n".format(fp))
try:
with open(fp, "rb") as f:
ret = f.read().decode("utf-8", "replace").strip()
@@ -266,7 +271,7 @@ def get_fk_salt() -> str:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = base64.b64encode(os.urandom(18))
ret = b64enc(os.urandom(18))
with open(fp, "wb") as f:
f.write(ret + b"\n")
@@ -279,7 +284,7 @@ def get_dk_salt() -> str:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = base64.b64encode(os.urandom(30))
ret = b64enc(os.urandom(30))
with open(fp, "wb") as f:
f.write(ret + b"\n")
@@ -292,7 +297,7 @@ def get_ah_salt() -> str:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = base64.b64encode(os.urandom(18))
ret = b64enc(os.urandom(18))
with open(fp, "wb") as f:
f.write(ret + b"\n")
@@ -322,8 +327,7 @@ def ensure_locale() -> None:
def ensure_webdeps() -> None:
ap = os.path.join(E.mod, "web/deps/mini-fa.woff")
if os.path.exists(ap):
if has_resource(E, "web/deps/mini-fa.woff"):
return
warn(
@@ -350,7 +354,7 @@ def configure_ssl_ver(al: argparse.Namespace) -> None:
# oh man i love openssl
# check this out
# hold my beer
assert ssl # type: ignore
assert ssl # type: ignore # !rm
ptn = re.compile(r"^OP_NO_(TLS|SSL)v")
sslver = terse_sslver(al.ssl_ver).split(",")
flags = [k for k in ssl.__dict__ if ptn.match(k)]
@@ -384,7 +388,7 @@ def configure_ssl_ver(al: argparse.Namespace) -> None:
def configure_ssl_ciphers(al: argparse.Namespace) -> None:
assert ssl # type: ignore
assert ssl # type: ignore # !rm
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
if al.ssl_ver:
ctx.options &= ~al.ssl_flags_en
@@ -516,14 +520,18 @@ def sfx_tpoke(top: str):
def showlic() -> None:
p = os.path.join(E.mod, "res", "COPYING.txt")
if not os.path.exists(p):
try:
with load_resource(E, "res/COPYING.txt") as f:
buf = f.read()
except:
buf = b""
if buf:
print(buf.decode("utf-8", "replace"))
else:
print("no relevant license info to display")
return
with open(p, "rb") as f:
print(f.read().decode("utf-8", "replace"))
def get_sects():
return [
@@ -772,7 +780,7 @@ def get_sects():
dedent(
"""
specify --exp or the "exp" volflag to enable placeholder expansions
in README.md / .prologue.html / .epilogue.html
in README.md / PREADME.md / .prologue.html / .epilogue.html
--exp-md (volflag exp_md) holds the list of placeholders which can be
expanded in READMEs, and --exp-lg (volflag exp_lg) likewise for logues;
@@ -888,7 +896,7 @@ def get_sects():
dedent(
"""
the mDNS protocol is multicast-based, which means there are thousands
of fun and intersesting ways for it to break unexpectedly
of fun and interesting ways for it to break unexpectedly
things to check if it does not work at all:
@@ -992,10 +1000,12 @@ def add_upload(ap):
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")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (within same filesystem) (volflag=hardlink)")
ap2.add_argument("--never-symlink", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=neversymlink)")
ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead (volflag=copydupes)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash")
ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440.0, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)")
@@ -1066,6 +1076,7 @@ def add_cert(ap, cert_path):
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, default="", help="bypass the copyparty authentication checks and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (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-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")
@@ -1073,6 +1084,10 @@ def add_auth(ap):
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")
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)")
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("--ipu", metavar="CIDR=USR", type=u, action="append", help="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]")
def add_chpw(ap):
@@ -1166,7 +1181,7 @@ def add_smb(ap):
ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)")
ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)")
ap2.add_argument("--smb-port", metavar="PORT", type=int, default=445, help="port to listen on -- if you change this value, you must NAT from TCP:445 to this port using iptables or similar")
ap2.add_argument("--smb-nwa-1", action="store_true", help="disable impacket#1433 workaround (truncate directory listings to 64kB)")
ap2.add_argument("--smb-nwa-1", action="store_true", help="truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance")
ap2.add_argument("--smb-nwa-2", action="store_true", help="disable impacket workaround for filecopy globs")
ap2.add_argument("--smba", action="store_true", help="small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)")
ap2.add_argument("--smbv", action="store_true", help="verbose")
@@ -1225,6 +1240,7 @@ def add_optouts(ap):
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
@@ -1240,7 +1256,7 @@ def add_safety(ap):
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
@@ -1285,6 +1301,7 @@ def add_logging(ap):
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")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
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=1, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
@@ -1343,7 +1360,7 @@ def add_transcoding(ap):
def add_db_general(ap, hcores):
noidx = APPLESAN_TXT if MACOS else ""
ap2 = ap.add_argument_group('general db options')
ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplication")
ap2.add_argument("-e2d", action="store_true", help="enable up2k database; this enables file search, upload-undo, improves deduplication")
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m")
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m")
ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db")
@@ -1352,11 +1369,13 @@ def add_db_general(ap, hcores):
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("--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-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")
ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (volflag=xlink)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
@@ -1435,7 +1454,7 @@ def add_ui(ap, retry):
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("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README.md documents (volflags: no_sb_md | sb_md)")
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
@@ -1459,16 +1478,18 @@ def add_debug(ap):
ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m")
ap2.add_argument("--bf-nc", metavar="NUM", type=int, default=200, help="bak-flips: stop if there's more than \033[33mNUM\033[0m files at \033[33m--kf-dir\033[0m already; default: 6.3 GiB max (200*32M)")
ap2.add_argument("--bf-dir", metavar="PATH", type=u, default="bf", help="bak-flips: store corrupted chunks at \033[33mPATH\033[0m; default: folder named 'bf' wherever copyparty was started")
ap2.add_argument("--bf-log", metavar="PATH", type=u, default="", help="bak-flips: log corruption info to a textfile at \033[33mPATH\033[0m")
# fmt: on
def run_argparse(
argv: list[str], formatter: Any, retry: bool, nc: int
argv: list[str], formatter: Any, retry: bool, nc: int, verbose=True
) -> argparse.Namespace:
ap = argparse.ArgumentParser(
formatter_class=formatter,
usage=argparse.SUPPRESS,
prog="copyparty",
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
)
@@ -1486,7 +1507,7 @@ def run_argparse(
tty = os.environ.get("TERM", "").lower() == "linux"
srvname = get_srvname()
srvname = get_srvname(verbose)
add_general(ap, nc, srvname)
add_network(ap)
@@ -1553,16 +1574,13 @@ def run_argparse(
return ret
def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
def main(argv: Optional[list[str]] = None) -> None:
time.strptime("19970815", "%Y%m%d") # python#7980
if WINDOWS:
os.system("rem") # enables colors
init_E(E)
if rsrc: # pyz
E.mod = rsrc
if argv is None:
argv = sys.argv
@@ -1619,6 +1637,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
("--hdr-au-usr", "--idp-h-usr"),
("--idp-h-sep", "--idp-gsep"),
("--th-no-crop", "--th-crop=n"),
("--never-symlink", "--hardlink-only"),
]
for dk, nk in deprecated:
idx = -1
@@ -1643,7 +1662,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
argv.extend(["--qr"])
if ANYWIN or not os.geteuid():
# win10 allows symlinks if admin; can be unexpected
argv.extend(["-p80,443,3923", "--ign-ebind", "--no-dedup"])
argv.extend(["-p80,443,3923", "--ign-ebind"])
except:
pass
@@ -1665,7 +1684,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
try:
al = run_argparse(argv, fmtr, retry, nc)
dal = run_argparse([], fmtr, retry, nc)
dal = run_argparse([], fmtr, retry, nc, False)
break
except SystemExit:
raise
@@ -1749,7 +1768,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
print("error: python2 cannot --smb")
return
if sys.version_info < (3, 6):
if not PY36:
al.no_scandir = True
if not hasattr(os, "sendfile"):

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 14, 3)
CODENAME = "one step forward"
BUILD_DT = (2024, 8, 30)
VERSION = (1, 15, 7)
CODENAME = "fill the drives"
BUILD_DT = (2024, 10, 14)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -840,8 +840,10 @@ class AuthSrv(object):
# fwd-decl
self.vfs = VFS(log_func, "", "", AXS(), {})
self.acct: dict[str, str] = {}
self.iacct: dict[str, str] = {}
self.acct: dict[str, str] = {} # uname->pw
self.iacct: dict[str, str] = {} # pw->uname
self.ases: dict[str, str] = {} # uname->session
self.sesa: dict[str, str] = {} # session->uname
self.defpw: dict[str, str] = {}
self.grps: dict[str, list[str]] = {}
self.re_pwd: Optional[re.Pattern] = None
@@ -853,6 +855,7 @@ class AuthSrv(object):
self.idp_accs: dict[str, list[str]] = {} # username->groupnames
self.idp_usr_gh: dict[str, str] = {} # username->group-header-value (cache)
self.hid_cache: dict[str, str] = {}
self.mutex = threading.Lock()
self.reload()
@@ -921,7 +924,7 @@ class AuthSrv(object):
for un, gn in un_gn:
# if ap/vp has a user/group placeholder, make sure to keep
# track so the same user/gruop is mapped when setting perms;
# track so the same user/group is mapped when setting perms;
# otherwise clear un/gn to indicate it's a regular volume
src1 = src0.replace("${u}", un or "\n")
@@ -1529,7 +1532,7 @@ class AuthSrv(object):
if enshare:
import sqlite3
shv = VFS(self.log_func, "", shr, AXS(), {"d2d": True})
shv = VFS(self.log_func, "", shr, AXS(), {})
db_path = self.args.shr_db
db = sqlite3.connect(db_path)
@@ -1548,8 +1551,8 @@ class AuthSrv(object):
if s_pw:
# gotta reuse the "account" for all shares with this pw,
# so do a light scramble as this appears in the web-ui
zs = ub64enc(hashlib.sha512(s_pw.encode("utf-8")).digest())[4:16]
sun = "s_%s" % (zs.decode("utf-8"),)
zb = hashlib.sha512(s_pw.encode("utf-8")).digest()
sun = "s_%s" % (ub64enc(zb)[4:16].decode("ascii"),)
acct[sun] = s_pw
else:
sun = "*"
@@ -1654,8 +1657,12 @@ class AuthSrv(object):
promote = []
demote = []
for vol in vfs.all_vols.values():
zb = hashlib.sha512(afsenc(vol.realpath)).digest()
hid = base64.b32encode(zb).decode("ascii").lower()
hid = self.hid_cache.get(vol.realpath)
if not hid:
zb = hashlib.sha512(afsenc(vol.realpath)).digest()
hid = base64.b32encode(zb).decode("ascii").lower()
self.hid_cache[vol.realpath] = hid
vflag = vol.flags.get("hist")
if vflag == "-":
pass
@@ -1891,6 +1898,11 @@ class AuthSrv(object):
if len(zs) == 3: # fc5 => ffcc55
vol.flags["tcolor"] = "".join([x * 2 for x in zs])
if vol.flags.get("neversymlink"):
vol.flags["hardlinkonly"] = True # was renamed
if vol.flags.get("hardlinkonly"):
vol.flags["hardlink"] = True
for k1, k2 in IMPLICATIONS:
if k1 in vol.flags:
vol.flags[k2] = True
@@ -1995,9 +2007,6 @@ class AuthSrv(object):
for x in drop:
vol.flags.pop(x)
if vol.flags.get("neversymlink") and not vol.flags.get("hardlink"):
vol.flags["copydupes"] = True
# verify tags mentioned by -mt[mp] are used by -mte
local_mtp = {}
local_only_mtp = {}
@@ -2076,6 +2085,8 @@ class AuthSrv(object):
have_e2d = False
have_e2t = False
have_dedup = False
unsafe_dedup = []
t = "volumes and permissions:\n"
for zv in vfs.all_vols.values():
if not self.warn_anonwrite or verbosity < 5:
@@ -2108,6 +2119,11 @@ class AuthSrv(object):
if "e2t" in zv.flags:
have_e2t = True
if "dedup" in zv.flags:
have_dedup = True
if "e2d" not in zv.flags and "hardlink" not in zv.flags:
unsafe_dedup.append("/" + zv.vpath)
t += "\n"
if self.warn_anonwrite and verbosity > 4:
@@ -2120,10 +2136,17 @@ class AuthSrv(object):
self.log("\n\033[{}\033[0m\n".format(t))
if not have_e2t:
t = "hint: argument -e2ts enables multimedia indexing (artist/title/...)"
t = "hint: enable multimedia indexing (artist/title/...) with argument -e2ts"
self.log(t, 6)
else:
t = "hint: argument -e2dsa enables searching, upload-undo, and better deduplication"
t = "hint: enable searching and upload-undo with argument -e2dsa"
self.log(t, 6)
if unsafe_dedup:
t = "WARNING: symlink-based deduplication is enabled for some volumes, but without indexing. Please enable -e2dsa and/or --hardlink to avoid problems when moving/renaming files. Affected volumes: %s"
self.log(t % (", ".join(unsafe_dedup)), 3)
elif not have_dedup:
t = "hint: enable upload deduplication with --dedup (but see readme for consequences)"
self.log(t, 6)
zv, _ = vfs.get("/", "*", False, False)
@@ -2165,8 +2188,11 @@ class AuthSrv(object):
self.grps = grps
self.iacct = {v: k for k, v in acct.items()}
self.load_sessions()
self.re_pwd = None
pwds = [re.escape(x) for x in self.iacct.keys()]
pwds.extend(list(self.sesa))
if pwds:
if self.ah.on:
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
@@ -2241,6 +2267,72 @@ class AuthSrv(object):
cur.close()
db.close()
def load_sessions(self, quiet=False) -> None:
# mutex me
if self.args.no_ses:
self.ases = {}
self.sesa = {}
return
import sqlite3
ases = {}
blen = (self.args.ses_len // 4) * 4 # 3 bytes in 4 chars
blen = (blen * 3) // 4 # bytes needed for ses_len chars
db = sqlite3.connect(self.args.ses_db)
cur = db.cursor()
for uname, sid in cur.execute("select un, si from us"):
if uname in self.acct:
ases[uname] = sid
n = []
q = "insert into us values (?,?,?)"
for uname in self.acct:
if uname not in ases:
sid = ub64enc(os.urandom(blen)).decode("ascii")
cur.execute(q, (uname, sid, int(time.time())))
ases[uname] = sid
n.append(uname)
if n:
db.commit()
cur.close()
db.close()
self.ases = ases
self.sesa = {v: k for k, v in ases.items()}
if n and not quiet:
t = ", ".join(n[:3])
if len(n) > 3:
t += "..."
self.log("added %d new sessions (%s)" % (len(n), t))
def forget_session(self, broker: Optional["BrokerCli"], uname: str) -> None:
with self.mutex:
self._forget_session(uname)
if broker:
broker.ask("_reload_sessions").get()
def _forget_session(self, uname: str) -> None:
if self.args.no_ses:
return
import sqlite3
db = sqlite3.connect(self.args.ses_db)
cur = db.cursor()
cur.execute("delete from us where un = ?", (uname,))
db.commit()
cur.close()
db.close()
self.sesa.pop(self.ases.get(uname, ""), "")
self.ases.pop(uname, "")
def chpw(self, broker: Optional["BrokerCli"], uname, pw) -> tuple[bool, str]:
if not self.args.chpw:
return False, "feature disabled in server config"
@@ -2260,7 +2352,7 @@ class AuthSrv(object):
if hpw == self.acct[uname]:
return False, "that's already your password my dude"
if hpw in self.iacct:
if hpw in self.iacct or hpw in self.sesa:
return False, "password is taken"
with self.mutex:

View File

@@ -9,14 +9,14 @@ import queue
from .__init__ import CORES, TYPE_CHECKING
from .broker_mpw import MpWorker
from .broker_util import ExceptionalQueue, try_exec
from .broker_util import ExceptionalQueue, NotExQueue, try_exec
from .util import Daemon, mp
if TYPE_CHECKING:
from .svchub import SvcHub
if True: # pylint: disable=using-constant-test
from typing import Any
from typing import Any, Union
class MProcess(mp.Process):
@@ -76,6 +76,10 @@ class BrokerMp(object):
for _, proc in enumerate(self.procs):
proc.q_pend.put((0, "reload", []))
def reload_sessions(self) -> None:
for _, proc in enumerate(self.procs):
proc.q_pend.put((0, "reload_sessions", []))
def collector(self, proc: MProcess) -> None:
"""receive message from hub in other process"""
while True:
@@ -104,7 +108,7 @@ class BrokerMp(object):
if retq_id:
proc.q_pend.put((retq_id, "retq", rv))
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
# new non-ipc invoking managed service in hub
obj = self.hub

View File

@@ -11,7 +11,7 @@ import queue
from .__init__ import ANYWIN
from .authsrv import AuthSrv
from .broker_util import BrokerCli, ExceptionalQueue
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
from .httpsrv import HttpSrv
from .util import FAKE_MP, Daemon, HMaccas
@@ -94,6 +94,10 @@ class MpWorker(BrokerCli):
self.asrv.reload()
self.logw("mpw.asrv reloaded")
elif dest == "reload_sessions":
with self.asrv.mutex:
self.asrv.load_sessions()
elif dest == "listen":
self.httpsrv.listen(args[0], args[1])
@@ -110,7 +114,7 @@ class MpWorker(BrokerCli):
else:
raise Exception("what is " + str(dest))
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
retq = ExceptionalQueue(1)
retq_id = id(retq)
with self.retpend_mutex:

View File

@@ -5,7 +5,7 @@ import os
import threading
from .__init__ import TYPE_CHECKING
from .broker_util import BrokerCli, ExceptionalQueue, try_exec
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
from .httpsrv import HttpSrv
from .util import HMaccas
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
from .svchub import SvcHub
if True: # pylint: disable=using-constant-test
from typing import Any
from typing import Any, Union
class BrokerThr(BrokerCli):
@@ -34,6 +34,7 @@ class BrokerThr(BrokerCli):
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
self.httpsrv = HttpSrv(self, None)
self.reload = self.noop
self.reload_sessions = self.noop
def shutdown(self) -> None:
# self.log("broker", "shutting down")
@@ -42,19 +43,14 @@ class BrokerThr(BrokerCli):
def noop(self) -> None:
pass
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
# new ipc invoking managed service in hub
obj = self.hub
for node in dest.split("."):
obj = getattr(obj, node)
rv = try_exec(True, obj, *args)
# pretend we're broker_mp
retq = ExceptionalQueue(1)
retq.put(rv)
return retq
return NotExQueue(obj(*args)) # type: ignore
def say(self, dest: str, *args: Any) -> None:
if dest == "listen":
@@ -70,4 +66,4 @@ class BrokerThr(BrokerCli):
for node in dest.split("."):
obj = getattr(obj, node)
try_exec(False, obj, *args)
obj(*args) # type: ignore

View File

@@ -33,6 +33,18 @@ class ExceptionalQueue(Queue, object):
return rv
class NotExQueue(object):
"""
BrokerThr uses this instead of ExceptionalQueue; 7x faster
"""
def __init__(self, rv: Any) -> None:
self.rv = rv
def get(self) -> Any:
return self.rv
class BrokerCli(object):
"""
helps mypy understand httpsrv.broker but still fails a few levels deeper,
@@ -48,7 +60,7 @@ class BrokerCli(object):
def __init__(self) -> None:
pass
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
return ExceptionalQueue(1)
def say(self, dest: str, *args: Any) -> None:

View File

@@ -7,7 +7,7 @@ import shutil
import time
from .__init__ import ANYWIN
from .util import Netdev, runcmd, wrename, wunlink
from .util import Netdev, load_resource, runcmd, wrename, wunlink
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
@@ -29,13 +29,15 @@ def ensure_cert(log: "RootLogger", args) -> None:
i feel awful about this and so should they
"""
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
with load_resource(args.E, "res/insecure.pem") as f:
cert_insec = f.read()
cert_appdata = os.path.join(args.E.cfg, "cert.pem")
if not os.path.isfile(args.cert):
if cert_appdata != args.cert:
raise Exception("certificate file does not exist: " + args.cert)
shutil.copy(cert_insec, args.cert)
with open(args.cert, "wb") as f:
f.write(cert_insec)
with open(args.cert, "rb") as f:
buf = f.read()
@@ -50,7 +52,9 @@ def ensure_cert(log: "RootLogger", args) -> None:
raise Exception(m + "private key must appear before server certificate")
try:
if filecmp.cmp(args.cert, cert_insec):
with open(args.cert, "rb") as f:
active_cert = f.read()
if active_cert == cert_insec:
t = "using default TLS certificate; https will be insecure:\033[36m {}"
log("cert", t.format(args.cert), 3)
except:
@@ -151,14 +155,22 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
raise Exception("no useable cert found")
expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.5 > expiry
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
if expired:
raise Exception("old server-cert has expired")
for n in names:
if n not in inf["sans"]:
raise Exception("does not have {}".format(n))
if expired:
raise Exception("old server-cert has expired")
if not filecmp.cmp(args.cert, cert_insec):
with load_resource(args.E, "res/insecure.pem") as f:
cert_insec = f.read()
with open(args.cert, "rb") as f:
active_cert = f.read()
if active_cert and active_cert != cert_insec:
return
except Exception as ex:
log("cert", "will create new server-cert; {}".format(ex))

View File

@@ -2,7 +2,7 @@
from __future__ import print_function, unicode_literals
# awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' '
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nw p q s ss sss v z zv"
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
onedash = set(zs.split())
@@ -12,8 +12,9 @@ def vf_bmap() -> dict[str, str]:
"dav_auth": "davauth",
"dav_rt": "davrt",
"ed": "dots",
"never_symlink": "neversymlink",
"no_dedup": "copydupes",
"hardlink_only": "hardlinkonly",
"no_clone": "noclone",
"no_dirsz": "nodirsz",
"no_dupe": "nodupe",
"no_forget": "noforget",
"no_pipe": "nopipe",
@@ -23,6 +24,7 @@ def vf_bmap() -> dict[str, str]:
"no_athumb": "dathumb",
}
for k in (
"dedup",
"dotsrch",
"e2d",
"e2ds",
@@ -58,6 +60,7 @@ def vf_vmap() -> dict[str, str]:
"no_hash": "nohash",
"no_idx": "noidx",
"re_maxage": "scan",
"safe_dedup": "safededup",
"th_convt": "convt",
"th_size": "thsize",
"th_crop": "crop",
@@ -129,10 +132,12 @@ permdescs = {
flagcats = {
"uploads, general": {
"nodupe": "rejects existing files (instead of symlinking them)",
"hardlink": "does dedup with hardlinks instead of symlinks",
"neversymlink": "disables symlink fallback; full copy instead",
"copydupes": "disables dedup, always saves full copies of dupes",
"dedup": "enable symlink-based file deduplication",
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
"safededup": "verify on-disk data before using it for dedup",
"noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)",
"sparse": "force use of sparse files, mainly for s3-backed storage",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
"nosub": "forces all uploads into the top folder of the vfs",
@@ -159,7 +164,7 @@ flagcats = {
"lifetime=3600": "uploads are deleted after 1 hour",
},
"database, general": {
"e2d": "enable database; makes files searchable + enables upload dedup",
"e2d": "enable database; makes files searchable + enables upload-undo",
"e2ds": "scan writable folders for new files on startup; also sets -e2d",
"e2dsa": "scans all folders for new files on startup; also sets -e2d",
"e2t": "enable multimedia indexing; makes it possible to search for tags",
@@ -177,7 +182,7 @@ flagcats = {
"noforget": "don't forget files when deleted from disk",
"fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"xlink": "cross-volume dupe detection / linking",
"xlink": "cross-volume dupe detection / linking (dangerous)",
"xdev": "do not descend into other filesystems",
"xvol": "do not follow symlinks leaving the volume root",
"dotsrch": "show dotfiles in search results",

View File

@@ -119,7 +119,7 @@ class Fstab(object):
self.srctab = srctab
def relabel(self, path: str, nval: str) -> None:
assert self.tab
assert self.tab # !rm
self.cache = {}
if ANYWIN:
path = self._winpath(path)
@@ -156,7 +156,7 @@ class Fstab(object):
self.log("failed to build tab:\n{}".format(min_ex()), 3)
self.build_fallback()
assert self.tab
assert self.tab # !rm
ret = self.tab._find(path)[0]
if self.trusted or path == ret.vpath:
return ret.realpath.split("/")[0]
@@ -167,6 +167,6 @@ class Fstab(object):
if not self.tab:
self.build_fallback()
assert self.tab
assert self.tab # !rm
ret = self.tab._find(path)[0]
return ret.realpath

View File

@@ -76,6 +76,7 @@ class FtpAuth(DummyAuthorizer):
else:
raise AuthenticationFailed("banned")
args = self.hub.args
asrv = self.hub.asrv
uname = "*"
if username != "anonymous":
@@ -86,6 +87,9 @@ class FtpAuth(DummyAuthorizer):
uname = zs
break
if args.ipu and uname == "*":
uname = args.ipu_iu[args.ipu_nm.map(ip)]
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd
if g.lim:
@@ -163,7 +167,7 @@ class FtpFs(AbstractedFS):
t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1)
fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
fn = sanitize_fn(fn or "", "")
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if not vfs.realpath:

File diff suppressed because it is too large Load Diff

View File

@@ -59,6 +59,8 @@ class HttpConn(object):
self.asrv: AuthSrv = hsrv.asrv # mypy404
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
self.pipes: Util.CachedDict = hsrv.pipes # mypy404
self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu
self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm
self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm
self.xff_nm: Optional[NetMap] = hsrv.xff_nm
self.xff_lan: NetMap = hsrv.xff_lan # type: ignore
@@ -103,9 +105,6 @@ class HttpConn(object):
self.log_src = ("%s \033[%dm%d" % (ip, color, self.addr[1])).ljust(26)
return self.log_src
def respath(self, res_name: str) -> str:
return os.path.join(self.E.mod, "web", res_name)
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func(self.log_src, msg, c)
@@ -165,6 +164,7 @@ class HttpConn(object):
self.log_src = self.log_src.replace("[36m", "[35m")
try:
assert ssl # type: ignore # !rm
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(self.args.cert)
if self.args.ssl_ver:
@@ -190,7 +190,7 @@ class HttpConn(object):
if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"):
ciphers = self.s.shared_ciphers()
assert ciphers
assert ciphers # !rm
overlap = [str(y[::-1]) for y in ciphers]
self.log("TLS cipher overlap:" + "\n".join(overlap))
for k, v in [

View File

@@ -1,7 +1,6 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import base64
import math
import os
import re
@@ -67,14 +66,17 @@ from .util import (
Magician,
Netdev,
NetMap,
absreal,
build_netmap,
has_resource,
ipnorm,
load_ipu,
load_resource,
min_ex,
shut_socket,
spack,
start_log_thrs,
start_stackmon,
ub64enc,
)
if TYPE_CHECKING:
@@ -91,6 +93,11 @@ if not hasattr(socket, "AF_UNIX"):
setattr(socket, "AF_UNIX", -9001)
def load_jinja2_resource(E: EnvParams, name: str):
with load_resource(E, "web/" + name, "r") as f:
return f.read()
class HttpSrv(object):
"""
handles incoming connections using HttpConn to process http,
@@ -152,8 +159,9 @@ class HttpSrv(object):
self.u2idx_free: dict[str, U2idx] = {}
self.u2idx_n = 0
assert jinja2 # type: ignore # !rm
env = jinja2.Environment()
env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
jn = [
"splash",
"shares",
@@ -166,18 +174,20 @@ class HttpSrv(object):
"cf",
]
self.j2 = {x: env.get_template(x + ".html") for x in jn}
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
self.prism = os.path.exists(zs)
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
if self.args.ipu:
self.ipu_iu, self.ipu_nm = load_ipu(self.log, self.args.ipu)
else:
self.ipu_iu = self.ipu_nm = None
self.ipa_nm = build_netmap(self.args.ipa)
self.xff_nm = build_netmap(self.args.xff_src)
self.xff_lan = build_netmap("lan")
self.statics: set[str] = set()
self._build_statics()
self.ptn_cc = re.compile(r"[\x00-\x1f]")
self.ptn_hsafe = re.compile(r"[\x00-\x1f<>\"'&]")
self.uparam_cc_ok = set("doc move tree".split())
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
if not self.args.no_dav:
@@ -209,14 +219,6 @@ class HttpSrv(object):
except:
pass
def _build_statics(self) -> None:
for dp, _, df in os.walk(os.path.join(self.E.mod, "web")):
for fn in df:
ap = absreal(os.path.join(dp, fn))
self.statics.add(ap)
if ap.endswith(".gz"):
self.statics.add(ap[:-3])
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
ips = set()
for ip, _ in self.bound:
@@ -237,7 +239,7 @@ class HttpSrv(object):
if self.args.log_htp:
self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6)
assert self.tp_q
assert self.tp_q # !rm
for _ in range(n):
self.tp_q.put(None)
@@ -431,7 +433,7 @@ class HttpSrv(object):
)
def thr_poolw(self) -> None:
assert self.tp_q
assert self.tp_q # !rm
while True:
task = self.tp_q.get()
if not task:
@@ -543,8 +545,8 @@ class HttpSrv(object):
except:
pass
v = base64.urlsafe_b64encode(spack(b">xxL", int(v)))
self.cb_v = v.decode("ascii")[-4:]
# spack gives 4 lsb, take 3 lsb, get 4 ch
self.cb_v = ub64enc(spack(b">L", int(v))[1:]).decode("ascii")
self.cb_ts = time.time()
return self.cb_v

View File

@@ -88,7 +88,7 @@ class Metrics(object):
addg("cpp_total_bans", str(self.hsrv.nban), t)
if not args.nos_vst:
x = self.hsrv.broker.ask("up2k.get_state")
x = self.hsrv.broker.ask("up2k.get_state", True, "")
vs = json.loads(x.get())
nvidle = 0

View File

@@ -473,7 +473,7 @@ class MTag(object):
sv = str(zv).split("/")[0].strip().lstrip("0")
ret[sk] = sv or 0
# normalize key notation to rkeobo
# normalize key notation to rekobo
okey = ret.get("key")
if okey:
key = str(okey).replace(" ", "").replace("maj", "").replace("min", "m")

View File

@@ -12,7 +12,7 @@ from types import SimpleNamespace
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
from .authsrv import LEELOO_DALLAS, VFS
from .bos import bos
from .util import Daemon, min_ex, pybin, runhook
from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin
if True: # pylint: disable=using-constant-test
from typing import Any, Union
@@ -151,6 +151,8 @@ class SMB(object):
def _uname(self) -> str:
if self.noacc:
return LEELOO_DALLAS
if not self.asrv.acct:
return "*"
try:
# you found it! my single worst bit of code so far
@@ -189,7 +191,7 @@ class SMB(object):
vfs, rem = self.asrv.vfs.get(vpath, uname, *perms)
if not vfs.realpath:
raise Exception("unmapped vfs")
return vfs, vfs.canonical(rem)
return vfs, vjoin(vfs.realpath, rem)
def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]:
vpath = vpath.replace("\\", "/").lstrip("/")
@@ -213,7 +215,7 @@ class SMB(object):
sz = 112 * 2 # ['.', '..']
for n, fn in enumerate(ls):
if sz >= 64000:
t = "listing only %d of %d files (%d byte) in /%s; see impacket#1433"
t = "listing only %d of %d files (%d byte) in /%s for performance; see --smb-nwa-1"
warning(t, n, len(ls), sz, vpath)
break
@@ -242,6 +244,7 @@ class SMB(object):
t = "blocked write (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname))
ap = absreal(ap)
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
self.nlog,

View File

@@ -84,7 +84,7 @@ class SSDPr(object):
name = self.args.doctitle
zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid))
hc.reply(zs.encode("utf-8", "replace"))
return False # close connectino
return False # close connection
class SSDPd(MCast):

View File

@@ -594,3 +594,20 @@ def _get_bit(x: int, i: int) -> bool:
class DataTooLongError(ValueError):
pass
def qr2svg(qr: QrCode, border: int) -> str:
parts: list[str] = []
for y in range(qr.size):
sy = border + y
for x in range(qr.size):
if qr.modules[y][x]:
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
t = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
<rect width="100%" height="100%" fill="#F7F7F7"/>
<path d="{1}" fill="#111111"/>
</svg>
"""
return t.format(qr.size + border * 2, " ".join(parts))

View File

@@ -2,8 +2,6 @@
from __future__ import print_function, unicode_literals
import argparse
import base64
import calendar
import errno
import gzip
import logging
@@ -16,7 +14,7 @@ import string
import sys
import threading
import time
from datetime import datetime, timedelta
from datetime import datetime
# from inspect import currentframe
# print(currentframe().f_lineno)
@@ -62,12 +60,14 @@ from .util import (
alltrace,
ansi_re,
build_netmap,
load_ipu,
min_ex,
mp,
odfusion,
pybin,
start_log_thrs,
start_stackmon,
ub64enc,
)
if TYPE_CHECKING:
@@ -104,6 +104,7 @@ class SvcHub(object):
self.argv = argv
self.E: EnvParams = args.E
self.no_ansi = args.no_ansi
self.tz = UTC if args.log_utc else None
self.logf: Optional[typing.TextIO] = None
self.logf_base_fn = ""
self.is_dut = False # running in unittest; always False
@@ -118,7 +119,8 @@ class SvcHub(object):
self.httpsrv_up = 0
self.log_mutex = threading.Lock()
self.next_day = 0
self.cday = 0
self.cmon = 0
self.tstack = 0.0
self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
@@ -220,6 +222,14 @@ class SvcHub(object):
noch.update([x for x in zsl if x])
args.chpw_no = noch
if args.ipu:
iu, nm = load_ipu(self.log, args.ipu)
setattr(args, "ipu_iu", iu)
setattr(args, "ipu_nm", nm)
if not self.args.no_ses:
self.setup_session_db()
if args.shr:
self.setup_share_db()
@@ -368,6 +378,64 @@ class SvcHub(object):
self.broker = Broker(self)
def setup_session_db(self) -> None:
if not HAVE_SQLITE3:
self.args.no_ses = True
t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
self.log("root", t, 3)
return
import sqlite3
create = True
db_path = self.args.ses_db
self.log("root", "opening sessions-db %s" % (db_path,))
for n in range(2):
try:
db = sqlite3.connect(db_path)
cur = db.cursor()
try:
cur.execute("select count(*) from us").fetchone()
create = False
break
except:
pass
except Exception as ex:
if n:
raise
t = "sessions-db corrupt; deleting and recreating: %r"
self.log("root", t % (ex,), 3)
try:
cur.close() # type: ignore
except:
pass
try:
db.close() # type: ignore
except:
pass
os.unlink(db_path)
sch = [
r"create table kv (k text, v int)",
r"create table us (un text, si text, t0 int)",
# username, session-id, creation-time
r"create index us_un on us(un)",
r"create index us_si on us(si)",
r"create index us_t0 on us(t0)",
r"insert into kv values ('sver', 1)",
]
assert db # type: ignore # !rm
assert cur # type: ignore # !rm
if create:
for cmd in sch:
cur.execute(cmd)
self.log("root", "created new sessions-db")
db.commit()
cur.close()
db.close()
def setup_share_db(self) -> None:
al = self.args
if not HAVE_SQLITE3:
@@ -426,8 +494,8 @@ class SvcHub(object):
r"create index sh_t1 on sh(t1)",
]
assert db # type: ignore
assert cur # type: ignore
assert db # type: ignore # !rm
assert cur # type: ignore # !rm
if create:
dver = 2
modified = True
@@ -544,7 +612,7 @@ class SvcHub(object):
fng = []
t_ff = "transcode audio, create spectrograms, video thumbnails"
to_check = [
(HAVE_SQLITE3, "sqlite", "file and media indexing"),
(HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"),
(HAVE_PIL, "pillow", "image thumbnails (plenty fast)"),
(HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"),
(HAVE_WEBP, "pillow-webp", "create thumbnails as webp files"),
@@ -791,7 +859,7 @@ class SvcHub(object):
self.args.nc = min(self.args.nc, soft // 2)
def _logname(self) -> str:
dt = datetime.now(UTC)
dt = datetime.now(self.tz)
fn = str(self.args.lo)
for fs in "YmdHMS":
fs = "%" + fs
@@ -944,6 +1012,11 @@ class SvcHub(object):
self._reload(rescan_all_vols=rescan_all_vols, up2k=up2k)
def _reload_sessions(self) -> None:
with self.asrv.mutex:
self.asrv.load_sessions(True)
self.broker.reload_sessions()
def stop_thr(self) -> None:
while not self.stop_req:
with self.stop_cond:
@@ -1064,12 +1137,12 @@ class SvcHub(object):
return
with self.log_mutex:
zd = datetime.now(UTC)
dt = datetime.now(self.tz)
ts = self.log_dfmt % (
zd.year,
zd.month * 100 + zd.day,
(zd.hour * 100 + zd.minute) * 100 + zd.second,
zd.microsecond // self.log_div,
dt.year,
dt.month * 100 + dt.day,
(dt.hour * 100 + dt.minute) * 100 + dt.second,
dt.microsecond // self.log_div,
)
if c and not self.args.no_ansi:
@@ -1090,41 +1163,26 @@ class SvcHub(object):
if not self.args.no_logflush:
self.logf.flush()
now = time.time()
if int(now) >= self.next_day:
self._set_next_day()
if dt.day != self.cday or dt.month != self.cmon:
self._set_next_day(dt)
def _set_next_day(self) -> None:
if self.next_day and self.logf and self.logf_base_fn != self._logname():
def _set_next_day(self, dt: datetime) -> None:
if self.cday and self.logf and self.logf_base_fn != self._logname():
self.logf.close()
self._setup_logfile("")
dt = datetime.now(UTC)
# unix timestamp of next 00:00:00 (leap-seconds safe)
day_now = dt.day
while dt.day == day_now:
dt += timedelta(hours=12)
dt = dt.replace(hour=0, minute=0, second=0)
try:
tt = dt.utctimetuple()
except:
# still makes me hella uncomfortable
tt = dt.timetuple()
self.next_day = calendar.timegm(tt)
self.cday = dt.day
self.cmon = dt.month
def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
"""handles logging from all components"""
with self.log_mutex:
now = time.time()
if int(now) >= self.next_day:
dt = datetime.fromtimestamp(now, UTC)
dt = datetime.now(self.tz)
if dt.day != self.cday or dt.month != self.cmon:
zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n"
zs = zs.format(dt.strftime("%Y-%m-%d"))
print(zs, end="")
self._set_next_day()
self._set_next_day(dt)
if self.logf:
self.logf.write(zs)
@@ -1143,12 +1201,11 @@ class SvcHub(object):
else:
msg = "%s%s\033[0m" % (c, msg)
zd = datetime.fromtimestamp(now, UTC)
ts = self.log_efmt % (
zd.hour,
zd.minute,
zd.second,
zd.microsecond // self.log_div,
dt.hour,
dt.minute,
dt.second,
dt.microsecond // self.log_div,
)
msg = fmt % (ts, src, msg)
try:
@@ -1246,5 +1303,5 @@ class SvcHub(object):
zs = "{}\n{}".format(VERSIONS, alltrace())
zb = zs.encode("utf-8", "replace")
zb = gzip.compress(zb)
zs = base64.b64encode(zb).decode("ascii")
zs = ub64enc(zb).decode("ascii")
self.log("stacks", zs)

View File

@@ -105,7 +105,7 @@ def gen_hdr(
ret += spack(b"<LL", vsz, vsz)
# windows support (the "?" replace below too)
fn = sanitize_fn(fn, "/", [])
fn = sanitize_fn(fn, "/")
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested

View File

@@ -95,7 +95,7 @@ class TcpSrv(object):
continue
# binding 0.0.0.0 after :: fails on dualstack
# but is necessary on non-dualstakc
# but is necessary on non-dualstack
if successful_binds:
continue

View File

@@ -1,7 +1,6 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import base64
import hashlib
import logging
import os
@@ -27,6 +26,7 @@ from .util import (
min_ex,
runcmd,
statdir,
ub64enc,
vsplit,
wrename,
wunlink,
@@ -109,6 +109,9 @@ except:
HAVE_VIPS = False
th_dir_cache = {}
def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -> str:
# base16 = 16 = 256
# b64-lc = 38 = 1444
@@ -122,14 +125,20 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -
if ext in ffa and fmt[:2] in ("wf", "jf"):
fmt = fmt.replace("f", "")
rd += "\n" + fmt
h = hashlib.sha512(afsenc(rd)).digest()
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
rd = ("%s/%s/" % (b64[:2], b64[2:4])).lower() + b64
dcache = th_dir_cache
rd_key = rd + "\n" + fmt
rd = dcache.get(rd_key)
if not rd:
h = hashlib.sha512(afsenc(rd_key)).digest()
b64 = ub64enc(h).decode("ascii")[:24]
rd = ("%s/%s/" % (b64[:2], b64[2:4])).lower() + b64
if len(dcache) > 9001:
dcache.clear()
dcache[rd_key] = rd
# could keep original filenames but this is safer re pathlen
h = hashlib.sha512(afsenc(fn)).digest()
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
fn = ub64enc(h).decode("ascii")[:24]
if fmt in ("opus", "caf", "mp3"):
cat = "ac"
@@ -479,7 +488,7 @@ class ThumbSrv(object):
if c == crops[-1]:
raise
assert img # type: ignore
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:

View File

@@ -53,6 +53,8 @@ class U2idx(object):
self.log("your python does not have sqlite3; searching will be disabled")
return
assert sqlite3 # type: ignore # !rm
self.active_id = ""
self.active_cur: Optional["sqlite3.Cursor"] = None
self.cur: dict[str, "sqlite3.Cursor"] = {}
@@ -104,7 +106,7 @@ class U2idx(object):
if not HAVE_SQLITE3 or not self.args.shr:
return None
assert sqlite3 # type: ignore
assert sqlite3 # type: ignore # !rm
db = sqlite3.connect(self.args.shr_db, timeout=2, check_same_thread=False)
cur = db.cursor()
@@ -120,7 +122,7 @@ class U2idx(object):
if not HAVE_SQLITE3 or "e2d" not in vn.flags:
return None
assert sqlite3 # type: ignore
assert sqlite3 # type: ignore # !rm
ptop = vn.realpath
histpath = self.asrv.vfs.histtab.get(ptop)
@@ -467,5 +469,5 @@ class U2idx(object):
return
if identifier == self.active_id:
assert self.active_cur
assert self.active_cur # !rm
self.active_cur.connection.interrupt()

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,8 @@ from __future__ import print_function, unicode_literals
import argparse
import base64
import contextlib
import binascii
import codecs
import errno
import hashlib
import hmac
@@ -30,13 +31,20 @@ from collections import Counter
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
from queue import Queue
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS
from .__init__ import (
ANYWIN,
EXE,
MACOS,
PY2,
PY36,
TYPE_CHECKING,
VT100,
WINDOWS,
EnvParams,
)
from .__version__ import S_BUILD_DT, S_VERSION
from .stolen import surrogateescape
ub64dec = base64.urlsafe_b64decode
ub64enc = base64.urlsafe_b64encode
try:
from datetime import datetime, timezone
@@ -64,7 +72,7 @@ if PY2:
if sys.version_info >= (3, 7) or (
sys.version_info >= (3, 6) and platform.python_implementation() == "CPython"
PY36 and platform.python_implementation() == "CPython"
):
ODict = dict
else:
@@ -126,7 +134,7 @@ if True: # pylint: disable=using-constant-test
from collections.abc import Callable, Iterable
import typing
from typing import Any, Generator, Optional, Pattern, Protocol, Union
from typing import IO, Any, Generator, Optional, Pattern, Protocol, Union
try:
from typing import LiteralString
@@ -164,12 +172,8 @@ except ImportError:
if not PY2:
from io import BytesIO
from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote
else:
from StringIO import StringIO as BytesIO # type: ignore
from urllib import quote # type: ignore # pylint: disable=no-name-in-module
from urllib import unquote # type: ignore # pylint: disable=no-name-in-module
try:
@@ -216,7 +220,7 @@ else:
FS_ENCODING = sys.getfilesystemencoding()
SYMTIME = sys.version_info > (3, 6) and os.utime in os.supports_follow_symlinks
SYMTIME = PY36 and os.utime in os.supports_follow_symlinks
META_NOBOTS = '<meta name="robots" content="noindex, nofollow">\n'
@@ -260,6 +264,8 @@ IMPLICATIONS = [
["e2vu", "e2v"],
["e2vp", "e2v"],
["e2v", "e2d"],
["hardlink_only", "hardlink"],
["hardlink", "dedup"],
["tftpvv", "tftpv"],
["smbw", "smb"],
["smb1", "smb"],
@@ -284,6 +290,30 @@ if ANYWIN:
UNPLICATIONS = [["no_dav", "daw"]]
DAV_ALLPROP_L = [
"contentclass",
"creationdate",
"defaultdocument",
"displayname",
"getcontentlanguage",
"getcontentlength",
"getcontenttype",
"getlastmodified",
"href",
"iscollection",
"ishidden",
"isreadonly",
"isroot",
"isstructureddocument",
"lastaccessed",
"name",
"parentname",
"resourcetype",
"supportedlock",
]
DAV_ALLPROPS = set(DAV_ALLPROP_L)
MIMES = {
"opus": "audio/ogg; codecs=opus",
}
@@ -336,7 +366,7 @@ MAGIC_MAP = {"jpeg": "jpg"}
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime"
DEF_MTE = "circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash"
DEF_MTE = ".files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash"
DEF_MTH = ".vq,.aq,vc,ac,fmt,res,.fps"
@@ -438,7 +468,7 @@ def py_desc() -> str:
def _sqlite_ver() -> str:
assert sqlite3 # type: ignore
assert sqlite3 # type: ignore # !rm
try:
co = sqlite3.connect(":memory:")
cur = co.cursor()
@@ -486,17 +516,36 @@ VERSIONS = (
)
_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER)
__all__ = [
"mp",
"BytesIO",
"quote",
"unquote",
"SQLITE_VER",
"JINJA_VER",
"PYFTPD_VER",
"PARTFTPY_VER",
]
try:
_b64_enc_tl = bytes.maketrans(b"+/", b"-_")
_b64_dec_tl = bytes.maketrans(b"-_", b"+/")
def ub64enc(bs: bytes) -> bytes:
x = binascii.b2a_base64(bs, newline=False)
return x.translate(_b64_enc_tl)
def ub64dec(bs: bytes) -> bytes:
bs = bs.translate(_b64_dec_tl)
return binascii.a2b_base64(bs)
def b64enc(bs: bytes) -> bytes:
return binascii.b2a_base64(bs, newline=False)
def b64dec(bs: bytes) -> bytes:
return binascii.a2b_base64(bs)
zb = b">>>????"
zb2 = base64.urlsafe_b64encode(zb)
if zb2 != ub64enc(zb) or zb != ub64dec(zb2):
raise Exception("bad smoke")
except Exception as ex:
ub64enc = base64.urlsafe_b64encode # type: ignore
ub64dec = base64.urlsafe_b64decode # type: ignore
b64enc = base64.b64encode # type: ignore
b64dec = base64.b64decode # type: ignore
if not PY36:
print("using fallback base64 codec due to %r" % (ex,))
class Daemon(threading.Thread):
@@ -616,11 +665,15 @@ class HLog(logging.Handler):
class NetMap(object):
def __init__(self, ips: list[str], cidrs: list[str], keep_lo=False) -> None:
def __init__(
self, ips: list[str], cidrs: list[str], keep_lo=False, strict_cidr=False
) -> None:
"""
ips: list of plain ipv4/ipv6 IPs, not cidr
cidrs: list of cidr-notation IPs (ip/prefix)
"""
self.mutex = threading.Lock()
if "::" in ips:
ips = [x for x in ips if x != "::"] + list(
[x.split("/")[0] for x in cidrs if ":" in x]
@@ -647,7 +700,7 @@ class NetMap(object):
bip = socket.inet_pton(fam, ip.split("/")[0])
self.bip.append(bip)
self.b2sip[bip] = ip.split("/")[0]
self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, False)
self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, strict_cidr)
self.bip.sort(reverse=True)
@@ -658,8 +711,10 @@ class NetMap(object):
try:
return self.cache[ip]
except:
pass
with self.mutex:
return self._map(ip)
def _map(self, ip: str) -> str:
v6 = ":" in ip
ci = IPv6Address(ip) if v6 else IPv4Address(ip)
bip = next((x for x in self.bip if ci in self.b2net[x]), None)
@@ -736,6 +791,7 @@ class _Unrecv(object):
self.buf = buf + self.buf
# !rm.yes>
class _LUnrecv(object):
"""
with expensive debug logging
@@ -792,6 +848,9 @@ class _LUnrecv(object):
print(t.format(buf, self.buf))
# !rm.no>
Unrecv = _Unrecv
@@ -915,7 +974,6 @@ class ProgressPrinter(threading.Thread):
self.msg = ""
self.end = False
self.n = -1
self.start()
def run(self) -> None:
sigblock()
@@ -1029,7 +1087,7 @@ class MTHash(object):
if self.stop:
return nch, "", ofs0, chunk_sz
assert f
assert f # !rm
hashobj = hashlib.sha512()
while chunk_rem > 0:
with self.imutex:
@@ -1044,7 +1102,7 @@ class MTHash(object):
ofs += len(buf)
bdig = hashobj.digest()[:33]
udig = base64.urlsafe_b64encode(bdig).decode("utf-8")
udig = ub64enc(bdig).decode("ascii")
return nch, udig, ofs0, chunk_sz
@@ -1070,7 +1128,7 @@ class HMaccas(object):
self.cache = {}
zb = hmac.new(self.key, msg, hashlib.sha512).digest()
zs = base64.urlsafe_b64encode(zb)[: self.retlen].decode("utf-8")
zs = ub64enc(zb)[: self.retlen].decode("ascii")
self.cache[msg] = zs
return zs
@@ -1111,7 +1169,7 @@ class Magician(object):
return ret
mime = magic.from_file(fpath, mime=True)
mime = re.split("[; ]", mime, 1)[0]
mime = re.split("[; ]", mime, maxsplit=1)[0]
try:
return EXTS[mime]
except:
@@ -1401,18 +1459,13 @@ def min_ex(max_lines: int = 8, reverse: bool = False) -> str:
return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
@contextlib.contextmanager
def ren_open(
fname: str, *args: Any, **kwargs: Any
) -> Generator[dict[str, tuple[typing.IO[Any], str]], None, None]:
def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str]:
fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None)
if fname == os.devnull:
with fun(fname, *args, **kwargs) as f:
yield {"orz": (f, fname)}
return
return fun(fname, *args, **kwargs), fname
if suffix:
ext = fname.split(".")[-1]
@@ -1434,6 +1487,7 @@ def ren_open(
asciified = False
b64 = ""
while True:
f = None
try:
if fdir:
fpath = os.path.join(fdir, fname)
@@ -1445,19 +1499,20 @@ def ren_open(
fname += suffix
ext += suffix
with fun(fsenc(fpath), *args, **kwargs) as f:
if b64:
assert fdir
fp2 = "fn-trunc.%s.txt" % (b64,)
fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8"))
f = fun(fsenc(fpath), *args, **kwargs)
if b64:
assert fdir # !rm
fp2 = "fn-trunc.%s.txt" % (b64,)
fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8"))
yield {"orz": (f, fname)}
return
return f, fname
except OSError as ex_:
ex = ex_
if f:
f.close()
# EPERM: android13
if ex.errno in (errno.EINVAL, errno.EPERM) and not asciified:
@@ -1478,8 +1533,7 @@ def ren_open(
if not b64:
zs = ("%s\n%s" % (orig_name, suffix)).encode("utf-8", "replace")
zs = hashlib.sha512(zs).digest()[:12]
b64 = base64.urlsafe_b64encode(zs).decode("utf-8")
b64 = ub64enc(hashlib.sha512(zs).digest()[:12]).decode("ascii")
badlen = len(fname)
while len(fname) >= badlen:
@@ -1712,7 +1766,7 @@ class MultipartParser(object):
returns the value of the next field in the multipart body,
raises if the field name is not as expected
"""
assert self.gen
assert self.gen # !rm
p_field, p_fname, p_data = next(self.gen)
if p_field != field_name:
raise WrongPostKey(field_name, p_field, p_fname, p_data)
@@ -1721,7 +1775,7 @@ class MultipartParser(object):
def drop(self) -> None:
"""discards the remaining multipart body"""
assert self.gen
assert self.gen # !rm
for _, _, data in self.gen:
for _ in data:
pass
@@ -1785,9 +1839,8 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
nc = rnd + extra
nb = (6 + 6 * nc) // 8
zb = os.urandom(nb)
zb = base64.urlsafe_b64encode(zb)
fn = zb[:nc].decode("utf-8") + ext
zb = ub64enc(os.urandom(nb))
fn = zb[:nc].decode("ascii") + ext
ok = not os.path.exists(fsenc(os.path.join(fdir, fn)))
return fn
@@ -1800,7 +1853,7 @@ def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str
zs = "%s %s" % (salt, fspath)
zb = zs.encode("utf-8", "replace")
return base64.urlsafe_b64encode(hashlib.sha512(zb).digest()).decode("ascii")
return ub64enc(hashlib.sha512(zb).digest()).decode("ascii")
def gen_filekey_dbg(
@@ -1814,7 +1867,7 @@ def gen_filekey_dbg(
) -> str:
ret = gen_filekey(alg, salt, fspath, fsize, inode)
assert log_ptn
assert log_ptn # !rm
if log_ptn.search(fspath):
try:
import inspect
@@ -1935,13 +1988,10 @@ def undot(path: str) -> str:
return "/".join(ret)
def sanitize_fn(fn: str, ok: str, bad: list[str]) -> str:
def sanitize_fn(fn: str, ok: str) -> str:
if "/" not in ok:
fn = fn.replace("\\", "/").split("/")[-1]
if fn.lower() in bad:
fn = "_" + fn
if ANYWIN:
remap = [
["<", ""],
@@ -1967,9 +2017,9 @@ def sanitize_fn(fn: str, ok: str, bad: list[str]) -> str:
return fn.strip()
def sanitize_vpath(vp: str, ok: str, bad: list[str]) -> str:
def sanitize_vpath(vp: str, ok: str) -> str:
parts = vp.replace(os.sep, "/").split("/")
ret = [sanitize_fn(x, ok, bad) for x in parts]
ret = [sanitize_fn(x, ok) for x in parts]
return "/".join(ret)
@@ -2073,6 +2123,8 @@ def html_bescape(s: bytes, quot: bool = False, crlf: bool = False) -> bytes:
def _quotep2(txt: str) -> str:
"""url quoter which deals with bytes correctly"""
if not txt:
return ""
btxt = w8enc(txt)
quot = quote(btxt, safe=b"/")
return w8dec(quot.replace(b" ", b"+")) # type: ignore
@@ -2080,18 +2132,61 @@ def _quotep2(txt: str) -> str:
def _quotep3(txt: str) -> str:
"""url quoter which deals with bytes correctly"""
if not txt:
return ""
btxt = w8enc(txt)
quot = quote(btxt, safe=b"/").encode("utf-8")
return w8dec(quot.replace(b" ", b"+"))
quotep = _quotep3 if not PY2 else _quotep2
if not PY2:
_uqsb = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~/"
_uqtl = {
n: ("%%%02X" % (n,) if n not in _uqsb else chr(n)).encode("utf-8")
for n in range(256)
}
_uqtl[b" "] = b"+"
def _quotep3b(txt: str) -> str:
"""url quoter which deals with bytes correctly"""
if not txt:
return ""
btxt = w8enc(txt)
if btxt.rstrip(_uqsb):
lut = _uqtl
btxt = b"".join([lut[ch] for ch in btxt])
return w8dec(btxt)
quotep = _quotep3b
_hexd = "0123456789ABCDEFabcdef"
_hex2b = {(a + b).encode(): bytes.fromhex(a + b) for a in _hexd for b in _hexd}
def unquote(btxt: bytes) -> bytes:
h2b = _hex2b
parts = iter(btxt.split(b"%"))
ret = [next(parts)]
for item in parts:
c = h2b.get(item[:2])
if c is None:
ret.append(b"%")
ret.append(item)
else:
ret.append(c)
ret.append(item[2:])
return b"".join(ret)
from urllib.parse import quote_from_bytes as quote
else:
from urllib import quote # type: ignore # pylint: disable=no-name-in-module
from urllib import unquote # type: ignore # pylint: disable=no-name-in-module
quotep = _quotep2
def unquotep(txt: str) -> str:
"""url unquoter which deals with bytes correctly"""
btxt = w8enc(txt)
# btxt = btxt.replace(b"+", b" ")
unq2 = unquote(btxt)
return w8dec(unq2)
@@ -2237,12 +2332,12 @@ w8enc = _w8enc3 if not PY2 else _w8enc2
def w8b64dec(txt: str) -> str:
"""decodes base64(filesystem-bytes) to wtf8"""
return w8dec(base64.urlsafe_b64decode(txt.encode("ascii")))
return w8dec(ub64dec(txt.encode("ascii")))
def w8b64enc(txt: str) -> str:
"""encodes wtf8 to base64(filesystem-bytes)"""
return base64.urlsafe_b64encode(w8enc(txt)).decode("ascii")
return ub64enc(w8enc(txt)).decode("ascii")
if not PY2 and WINDOWS:
@@ -2400,7 +2495,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
def get_df(abspath: str) -> tuple[Optional[int], Optional[int]]:
try:
# some fuses misbehave
assert ctypes
assert ctypes # type: ignore # !rm
if ANYWIN:
bfree = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
@@ -2589,6 +2684,31 @@ def build_netmap(csv: str):
return NetMap(ips, cidrs, True)
def load_ipu(log: "RootLogger", ipus: list[str]) -> tuple[dict[str, str], NetMap]:
ip_u = {"": "*"}
cidr_u = {}
for ipu in ipus:
try:
cidr, uname = ipu.split("=")
cip, csz = cidr.split("/")
except:
t = "\n invalid value %r for argument --ipu; must be CIDR=UNAME (192.168.0.0/16=amelia)"
raise Exception(t % (ipu,))
uname2 = cidr_u.get(cidr)
if uname2 is not None:
t = "\n invalid value %r for argument --ipu; cidr %s already mapped to %r"
raise Exception(t % (ipu, cidr, uname2))
cidr_u[cidr] = uname
ip_u[cip] = uname
try:
nm = NetMap(["::"], list(cidr_u.keys()), True, True)
except Exception as ex:
t = "failed to translate --ipu into netmap, probably due to invalid config: %r"
log("root", t % (ex,), 1)
raise
return ip_u, nm
def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
readsz = min(bufsz, 128 * 1024)
with open(fsenc(fn), "rb", bufsz) as f:
@@ -2618,8 +2738,7 @@ def hashcopy(
if slp:
time.sleep(slp)
digest = hashobj.digest()[:33]
digest_b64 = base64.urlsafe_b64encode(digest).decode("utf-8")
digest_b64 = ub64enc(hashobj.digest()[:33]).decode("ascii")
return tlen, hashobj.hexdigest(), digest_b64
@@ -2859,7 +2978,7 @@ def getalive(pids: list[int], pgid: int) -> list[int]:
alive.append(pid)
else:
# windows doesn't have pgroups; assume
assert psutil
assert psutil # type: ignore # !rm
psutil.Process(pid)
alive.append(pid)
except:
@@ -2877,7 +2996,7 @@ def killtree(root: int) -> None:
pgid = 0
if HAVE_PSUTIL:
assert psutil
assert psutil # type: ignore # !rm
pids = [root]
parent = psutil.Process(root)
for child in parent.children(recursive=True):
@@ -3290,7 +3409,7 @@ def runhook(
at: float,
txt: str,
) -> dict[str, Any]:
assert broker or up2k
assert broker or up2k # !rm
asrv = (broker or up2k).asrv
args = (broker or up2k).args
vp = vp.replace("\\", "/")
@@ -3355,9 +3474,15 @@ def loadpy(ap: str, hot: bool) -> Any:
def gzip_orig_sz(fn: str) -> int:
with open(fsenc(fn), "rb") as f:
f.seek(-4, 2)
rv = f.read(4)
return sunpack(b"I", rv)[0] # type: ignore
return gzip_file_orig_sz(f)
def gzip_file_orig_sz(f) -> int:
start = f.tell()
f.seek(-4, 2)
rv = f.read(4)
f.seek(start, 0)
return sunpack(b"I", rv)[0] # type: ignore
def align_tab(lines: list[str]) -> list[str]:
@@ -3484,7 +3609,7 @@ def termsize() -> tuple[int, int]:
def hidedir(dp) -> None:
if ANYWIN:
try:
assert ctypes
assert ctypes # type: ignore # !rm
k32 = ctypes.WinDLL("kernel32")
attrs = k32.GetFileAttributesW(dp)
if attrs >= 0:
@@ -3493,6 +3618,110 @@ def hidedir(dp) -> None:
pass
try:
if sys.version_info < (3, 10):
# py3.8 doesn't have .files
# py3.9 has broken .is_file
raise ImportError()
import importlib.resources as impresources
except ImportError:
try:
import importlib_resources as impresources
except ImportError:
impresources = None
try:
if sys.version_info > (3, 10):
raise ImportError()
import pkg_resources
except ImportError:
pkg_resources = None
def _pkg_resource_exists(pkg: str, name: str) -> bool:
if not pkg_resources:
return False
try:
return pkg_resources.resource_exists(pkg, name)
except NotImplementedError:
return False
def stat_resource(E: EnvParams, name: str):
path = os.path.join(E.mod, name)
if os.path.exists(path):
return os.stat(fsenc(path))
return None
def _find_impresource(pkg: types.ModuleType, name: str):
assert impresources # !rm
try:
files = impresources.files(pkg)
except ImportError:
return None
return files.joinpath(name)
_rescache_has = {}
def _has_resource(name: str):
try:
return _rescache_has[name]
except:
pass
if len(_rescache_has) > 999:
_rescache_has.clear()
assert __package__ # !rm
pkg = sys.modules[__package__]
if impresources:
res = _find_impresource(pkg, name)
if res and res.is_file():
_rescache_has[name] = True
return True
if pkg_resources:
if _pkg_resource_exists(pkg.__name__, name):
_rescache_has[name] = True
return True
_rescache_has[name] = False
return False
def has_resource(E: EnvParams, name: str):
return _has_resource(name) or os.path.exists(os.path.join(E.mod, name))
def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
enc = None if "b" in mode else "utf-8"
if impresources:
assert __package__ # !rm
res = _find_impresource(sys.modules[__package__], name)
if res and res.is_file():
if enc:
return res.open(mode, encoding=enc)
else:
# throws if encoding= is mentioned at all
return res.open(mode)
if pkg_resources:
assert __package__ # !rm
pkg = sys.modules[__package__]
if _pkg_resource_exists(pkg.__name__, name):
stream = pkg_resources.resource_stream(pkg.__name__, name)
if enc:
stream = codecs.getreader(enc)(stream)
return stream
return open(os.path.join(E.mod, name), mode, encoding=enc)
class Pebkac(Exception):
def __init__(
self, code: int, msg: Optional[str] = None, log: Optional[str] = None
@@ -3520,3 +3749,16 @@ class WrongPostKey(Pebkac):
self.got = got
self.fname = fname
self.datagen = datagen
_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER)
__all__ = [
"mp",
"BytesIO",
"quote",
"unquote",
"SQLITE_VER",
"JINJA_VER",
"PYFTPD_VER",
"PARTFTPY_VER",
]

View File

@@ -64,7 +64,7 @@
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-b1: rgba(128,128,128,0.8);
--u2-tab-1-fg: #fd7;
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
--u2-tab-1-bg: linear-gradient(to bottom, #353, var(--bg) 80%);
--u2-tab-1-b1: #7c5;
--u2-tab-1-b2: #583;
--u2-tab-1-sh: #280;
@@ -345,6 +345,7 @@ html.cz {
--srv-3: #fff;
--u2-tab-b1: var(--bg-d3);
--u2-tab-1-bg: a;
}
html.cy {
--fg: #fff;
@@ -374,12 +375,17 @@ html.cy {
--btn-bs: 0 .25em 0 #f00;
--chk-fg: #fd0;
--txt-bg: #000;
--srv-1: #f00;
--srv-3: #fff;
--op-aa-bg: #fff;
--u2-b1-bg: #f00;
--u2-b2-bg: #f00;
--g-sel-fg: #fff;
--g-sel-bg: #aaa;
--g-fsel-bg: #aaa;
}
html.dz {
--fg: #4d4;
@@ -436,7 +442,7 @@ html.dz {
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-b1: var(--fg-weak);
--u2-tab-1-fg: #fff;
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
--u2-tab-1-bg: linear-gradient(to bottom, #151, var(--bg) 80%);
--u2-tab-1-b1: #7c5;
--u2-tab-1-b2: #583;
--u2-tab-1-sh: #280;
@@ -598,7 +604,7 @@ html.dy {
background: var(--sel-bg);
text-shadow: none;
}
html,body,tr,th,td,#files,a {
html,body,tr,th,td,#files,a,#blogout {
color: inherit;
background: none;
font-weight: inherit;
@@ -681,11 +687,15 @@ html.y #path {
#files tbody div a {
color: var(--tab-alt);
}
a, #files tbody div a:last-child {
a, #blogout, #files tbody div a:last-child {
color: var(--a);
padding: .2em;
text-decoration: none;
}
#blogout {
margin: -.2em;
}
#blogout:hover,
a:hover {
color: var(--a-hil);
background: var(--a-h-bg);
@@ -886,7 +896,7 @@ html.y #path a:hover {
max-width: 52em;
}
.mdo.sb,
#epi.logue.mdo>iframe {
.logue.mdo>iframe {
max-width: 54em;
}
.mdo,
@@ -929,6 +939,9 @@ html.y #path a:hover {
color: var(--srv-3);
border-bottom: 1px solid var(--srv-3b);
}
#flogout {
display: inline;
}
#goh+span {
color: var(--bg-u5);
padding-left: .5em;
@@ -1349,6 +1362,7 @@ html.y #widget.open {
}
#widget.cmp #barpos,
#widget.cmp #barbuf {
height: 1.6em;
width: calc(100% - 11em);
border-radius: 0;
left: 5em;
@@ -3112,18 +3126,30 @@ html.by #u2cards a.act {
html.cy #wrap {
color: #000;
}
html.cy .mdo a {
background: #f00;
}
html.cy #wrap,
html.cy #acc_info a,
html.cy #op_up2k,
html.cy #files,
html.cy #files a,
html.cy #files tbody div a:last-child {
color: #000;
}
html.cy #u2tab a,
html.cy #u2cards a {
color: #f00;
}
html.cy #unpost a {
color: #ff0;
}
html.cy #barbuf {
filter: hue-rotate(267deg) brightness(0.8) contrast(4);
}
html.cy #pvol {
filter: hue-rotate(4deg) contrast(2.2);
}

View File

@@ -108,12 +108,9 @@
{%- for f in files %}
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
{%- if f.tags is defined %}
{%- for k in taglist %}
<td>{{ f.tags[k] }}</td>
{%- endfor %}
{%- endif %}
<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
{%- if f.tags is defined %}
{%- for k in taglist %}<td>{{ f.tags[k] }}</td>{%- endfor %}
{%- endif %}<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
{%- endfor %}
</tbody>

View File

@@ -22,7 +22,7 @@ var Ls = {
"vq": "video quality / bitrate",
"pixfmt": "subsampling / pixel structure",
"resw": "horizontal resolution",
"resh": "veritcal resolution",
"resh": "vertical resolution",
"chs": "audio channels",
"hz": "sample rate"
},
@@ -90,10 +90,18 @@ var Ls = {
"m_ok": "OK",
"m_ng": "Cancel",
"ht_s": "second!s",
"ht_m": "minute!s",
"ht_h": "hour!s",
"ht_d": "day!s",
"enable": "Enable",
"danger": "DANGER",
"clipped": "copied to clipboard",
"ht_s1": "second",
"ht_s2": "seconds",
"ht_m1": "minute",
"ht_m2": "minutes",
"ht_h1": "hour",
"ht_h2": "hours",
"ht_d1": "day",
"ht_d2": "days",
"ht_and": " and ",
"goh": "control-panel",
@@ -170,6 +178,15 @@ var Ls = {
"utl_stat": "status",
"utl_prog": "progress",
// keep short:
"utl_404": "404",
"utl_err": "ERROR",
"utl_oserr": "OS-error",
"utl_found": "found",
"utl_defer": "defer",
"utl_yolo": "YOLO",
"utl_done": "done",
"ul_flagblk": "the files were added to the queue</b><br>however there is a busy up2k in another browser tab,<br>so waiting for that to finish first",
"ul_btnlk": "the server configuration has locked this switch into this state",
@@ -195,11 +212,13 @@ var Ls = {
"cl_hcancel": "column hiding aborted",
"ct_grid": '田 the grid',
"ct_ttips": '◔ ◡ ◔"> tooltips',
"ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs',
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
"ct_dots": 'show hidden files (if server permits)">dotfiles',
"ct_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_readme": 'show README.md in folder listings">📜 readme',
"ct_idxh": 'show index.html instead of folder listing">htm',
"ct_sbars": 'show scrollbars">⟊',
@@ -311,7 +330,7 @@ var Ls = {
"fs_sc": "share the folder you're in",
"fs_ss": "share the selected files",
"fs_just1d": "you cannot select more than one folder,\nor mix flies and folders in one selection",
"fs_just1d": "you cannot select more than one folder,\nor mix files and folders in one selection",
"fs_abrt": "❌ abort",
"fs_rand": "🎲 rand.name",
"fs_go": "✅ create share",
@@ -326,7 +345,8 @@ var Ls = {
"fs_pname": "optional link name; will be random if blank",
"fs_tsrc": "the file or folder to share",
"fs_ppwd": "optional password",
"fs_ok": "<h6>share-URL created</h6>\npress <code>Enter/OK</code> to Clipboard\npress <code>ESC/Cancel</code> to Close\n\n",
"fs_w8": "creating share...",
"fs_ok": "press <code>Enter/OK</code> to Clipboard\npress <code>ESC/Cancel</code> to Close",
"frt_dec": "may fix some cases of broken filenames\">url-decode",
"frt_rst": "reset modified filenames back to the original ones\">↺ reset",
@@ -336,6 +356,8 @@ var Ls = {
"fr_case": "case-sensitive regex\">case",
"fr_win": "windows-safe names; replace <code>&lt;&gt;:&quot;\\|?*</code> with japanese fullwidth characters\">win",
"fr_slash": "replace <code>/</code> with a character that doesn't cause new folders to be created\">no /",
"fr_re": "regex search pattern to apply to original filenames; capturing groups can be referenced in the format field below like &lt;code&gt;(1)&lt;/code&gt; and &lt;code&gt;(2)&lt;/code&gt; and so on",
"fr_fmt": "inspired by foobar2000:$N&lt;code&gt;(title)&lt;/code&gt; is replaced by song title,$N&lt;code&gt;[(artist) - ](title)&lt;/code&gt; skips [this] part if artist is blank$N&lt;code&gt;$lpad((tn),2,0)&lt;/code&gt; pads tracknumber to 2 digits",
"fr_pdel": "delete",
"fr_pnew": "save as",
"fr_pname": "provide a name for your new preset",
@@ -345,7 +367,7 @@ var Ls = {
"fr_tags": "tags for the selected files (read-only, just for reference):",
"fr_busy": "renaming {0} items...\n\n{1}",
"fr_efail": "rename failed:\n",
"fr_nchg": "{0} of the new names were altered due to <code>win</code> and/or <code>ikke /</code>\n\nOK to continue with these altered new names?",
"fr_nchg": "{0} of the new names were altered due to <code>win</code> and/or <code>no /</code>\n\nOK to continue with these altered new names?",
"fd_ok": "delete OK",
"fd_err": "delete failed:\n",
@@ -395,6 +417,7 @@ var Ls = {
"gt_c1": "truncate filenames more (show less)",
"gt_c2": "truncate filenames less (show more)",
"sm_w8": "searching...",
"sm_prev": "search results below are from a previous query:\n ",
"sl_close": "close search results",
"sl_hits": "showing {0} hits",
@@ -467,16 +490,20 @@ var Ls = {
"un_del": "delete",
"un_m3": "loading your recent uploads...",
"un_busy": "deleting {0} files...",
"un_clip": "{0} links copied to clipboard",
"u_https1": "you should",
"u_https2": "switch to https",
"u_https3": "for better performance",
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>',
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",
"u_enpot": 'switch to <a href="#">potato UI</a> (may improve upload speed)',
"u_depot": 'switch to <a href="#">fancy UI</a> (may reduce upload speed)',
"u_gotpot": 'switching to the potato UI for improved upload speed,\n\nfeel free to disagree and switch back!',
"u_pott": "<p>files: &nbsp; <b>{0}</b> finished, &nbsp; <b>{1}</b> failed, &nbsp; <b>{2}</b> busy, &nbsp; <b>{3}</b> queued</p>",
"u_ever": "this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
"u_su2k": 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better',
"u_ewrite": 'you do not have write-access to this folder',
@@ -489,10 +516,16 @@ var Ls = {
"u_up_life": "This upload will be deleted from the server\n{0} after it completes",
"u_asku": 'upload these {0} files to <code>{1}</code>',
"u_unpt": "you can undo / delete this upload using the top-left 🧯",
"u_bigtab": 'about to show {0} files\n\nthis may crash your browser, are you sure?',
"u_scan": 'Scanning files...',
"u_dirstuck": 'directory iterator got stuck trying to access the following {0} items; will skip:',
"u_etadone": 'Done ({0}, {1} files)',
"u_etaprep": '(preparing to upload)',
"u_hashdone": 'hashing done',
"u_hashing": 'hash',
"u_hs": 'handshaking...',
"u_dupdefer": "duplicate; will be processed after all other files",
"u_actx": "click this text to prevent loss of<br />performance when switching to other windows/tabs",
"u_fixed": "OK!&nbsp; Fixed it 👍",
"u_cuerr": "failed to upload chunk {0} of {1};\nprobably harmless, continuing\n\nfile: {2}",
"u_cuerr2": "server rejected upload (chunk {0} of {1});\nwill retry later\n\nfile: {2}\n\nerror ",
@@ -626,10 +659,18 @@ var Ls = {
"m_ok": "OK",
"m_ng": "Avbryt",
"ht_s": "sekund!er",
"ht_m": "minutt!er",
"ht_h": "time!r",
"ht_d": "dag!er",
"enable": "Aktiv",
"danger": "VARSKU",
"clipped": "kopiert til utklippstavlen",
"ht_s1": "sekund",
"ht_s2": "sekunder",
"ht_m1": "minutt",
"ht_m2": "minutter",
"ht_h1": "time",
"ht_h2": "timer",
"ht_d1": "dag",
"ht_d2": "dager",
"ht_and": " og ",
"goh": "kontrollpanel",
@@ -666,7 +707,7 @@ var Ls = {
"wt_selinv": "inverter utvalg",
"wt_selzip": "last ned de valgte filene som et arkiv",
"wt_seldl": "last ned de valgte filene$NSnarvei: Y",
"wt_npirc": "kopiér sang-info (irc-formattert)",
"wt_npirc": "kopiér sang-info (irc-formatert)",
"wt_nptxt": "kopiér sang-info",
"wt_grid": "bytt mellom ikoner og listevisning$NSnarvei: G",
"wt_prev": "forrige sang$NSnarvei: J",
@@ -706,6 +747,15 @@ var Ls = {
"utl_stat": "status",
"utl_prog": "fremdrift",
// må være korte:
"utl_404": "404",
"utl_err": "FEIL!",
"utl_oserr": "OS-feil",
"utl_found": "funnet",
"utl_defer": "senere",
"utl_yolo": "YOLO",
"utl_done": "ferdig",
"ul_flagblk": "filene har blitt lagt i køen</b><br>men det er en annen nettleserfane som holder på med befaring eller opplastning akkurat nå,<br>så venter til den er ferdig først",
"ul_btnlk": "bryteren har blitt låst til denne tilstanden i serverens konfigurasjon",
@@ -731,11 +781,13 @@ var Ls = {
"cl_hcancel": "kolonne-skjuling avbrutt",
"ct_grid": '田 ikoner',
"ct_ttips": 'hvis 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⮯',
"ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig',
"ct_dir1st": 'sorter slik at mapper kommer foran filer">📁 først',
"ct_nsort": 'naturlig sortering (forstår tall i filnavn)">nsort',
"ct_readme": 'vis README.md nedenfor filene">📜 readme',
"ct_idxh": 'vis index.html istedenfor fil-liste">htm',
"ct_sbars": 'vis rullgardiner / skrollefelt">⟊',
@@ -834,7 +886,7 @@ var Ls = {
"f_dls": 'linkene i denne mappen er nå\nomgjort til nedlastningsknapper',
"f_partial": "For å laste ned en fil som enda ikke er ferdig opplastet, klikk på filen som har samme filnavn som denne, men uten <code>.PARTIAL</code> på slutten. Da vil serveren passe på at nedlastning går bra. Derfor anbefales det sterkt å trykke ABRYT eller Escape-tasten.\n\nHvis du virkelig ønsker å laste ned denne <code>.PARTIAL</code>-filen på en ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høyst sannsynlig motta korrupt data.",
"f_partial": "For å laste ned en fil som enda ikke er ferdig opplastet, klikk på filen som har samme filnavn som denne, men uten <code>.PARTIAL</code> på slutten. Da vil serveren passe på at nedlastning går bra. Derfor anbefales det sterkt å trykke AVBRYT eller Escape-tasten.\n\nHvis du virkelig ønsker å laste ned denne <code>.PARTIAL</code>-filen på en ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høyst sannsynlig motta korrupt data.",
"ft_paste": "Lim inn {0} filer$NSnarvei: ctrl-V",
"fr_eperm": 'kan ikke endre navn:\ndu har ikke “move”-rettigheten i denne mappen',
@@ -862,7 +914,8 @@ var Ls = {
"fs_pname": "frivillig navn (blir noe tilfeldig ellers)",
"fs_tsrc": "fil/mappe som skal deles",
"fs_ppwd": "frivillig passord",
"fs_ok": "<h6>URL opprettet</h6>\ntrykk <code>Enter/OK</code> for å kopiere linken (for CTRL-V)\ntrykk <code>ESC/Avbryt</code> for å bare bekrefte\n\n",
"fs_w8": "oppretter deling...",
"fs_ok": "trykk <code>Enter/OK</code> for å kopiere linken (for CTRL-V)\ntrykk <code>ESC/Avbryt</code> for å bare bekrefte",
"frt_dec": "kan korrigere visse ødelagte filnavn\">url-decode",
"frt_rst": "nullstiller endringer (tilbake til de originale filnavnene)\">↺ reset",
@@ -872,6 +925,8 @@ var Ls = {
"fr_case": "versalfølsomme uttrykk\">Aa",
"fr_win": "bytt ut bokstavene <code>&lt;&gt;:&quot;\\|?*</code> med$Ntilsvarende som windows ikke får panikk av\">win",
"fr_slash": "bytt ut bokstaven <code>/</code> slik at den ikke forårsaker at nye mapper opprettes\">ikke /",
"fr_re": "regex-mønster som kjøres på hvert filnavn. Grupper kan leses ut i format-feltet nedenfor, f.eks. &lt;code&gt;(1)&lt;/code&gt; og &lt;code&gt;(2)&lt;/code&gt; osv.",
"fr_fmt": "inspirert av foobar2000:$N&lt;code&gt;(title)&lt;/code&gt; byttes ut med sangtittel,$N&lt;code&gt;[(artist) - ](title)&lt;/code&gt; dropper [dette] hvis artist er blank$N&lt;code&gt;$lpad((tn),2,0)&lt;/code&gt; viser sangnr. med 2 siffer",
"fr_pdel": "slett",
"fr_pnew": "lagre som",
"fr_pname": "gi innstillingene dine et navn",
@@ -931,6 +986,7 @@ var Ls = {
"gt_c1": "reduser maks-lengde på filnavn",
"gt_c2": "øk maks-lengde på filnavn",
"sm_w8": "søker...",
"sm_prev": "søkeresultatene er fra et tidligere søk:\n ",
"sl_close": "lukk søkeresultater",
"sl_hits": "viser {0} treff",
@@ -1003,16 +1059,20 @@ var Ls = {
"un_del": "slett",
"un_m3": "henter listen med nylig opplastede filer...",
"un_busy": "sletter {0} filer...",
"un_clip": "{0} lenker kopiert til utklippstavlen",
"u_https1": "du burde",
"u_https2": "bytte til https",
"u_https3": "for høyere hastighet",
"u_ancient": 'nettleseren din er prehistorisk -- mulig du burde <a href="#" onclick="goto(\'bup\')">bruke bup istedenfor</a>',
"u_nowork": "krever firefox 53+, chrome 57+, eller iOS 11+",
"u_nodrop": 'nettleseren din er for gammel til å laste opp filer ved å dra dem inn i vinduet',
"u_notdir": "mottok ikke mappen!\n\nnettleseren din er for gammel,\nprøv å dra mappen inn i vinduet istedenfor",
"u_uri": "for å laste opp bilder ifra andre nettleservinduer,\nslipp bildet rett på den store last-opp-knappen",
"u_enpot": 'bytt til <a href="#">enkelt UI</a> (gir sannsynlig raskere opplastning)',
"u_depot": 'bytt til <a href="#">snæsent UI</a> (gir sannsynlig tregere opplastning)',
"u_gotpot": 'byttet til et enklere UI for å laste opp raskere,\n\ndu kan gjerne bytte tilbake altså!',
"u_pott": "<p>filer: &nbsp; <b>{0}</b> ferdig, &nbsp; <b>{1}</b> feilet, &nbsp; <b>{2}</b> behandles, &nbsp; <b>{3}</b> i kø</p>",
"u_ever": "dette er den primitive opplasteren; up2k krever minst:<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
"u_su2k": 'dette er den primitive opplasteren; <a href="#" id="u2yea">up2k</a> er bedre',
"u_ewrite": 'du har ikke skrivetilgang i denne mappen',
@@ -1025,10 +1085,16 @@ var Ls = {
"u_up_life": "Filene slettes fra serveren {0}\netter at opplastningen er fullført",
"u_asku": 'Laste opp disse {0} filene til <code>{1}</code>',
"u_unpt": "Du kan angre / slette opplastningen med 🧯 oppe til venstre",
"u_bigtab": 'Vil nå vise {0} filer...\n\nDette kan krasje nettleseren din. Fortsette?',
"u_scan": 'Leser mappene...',
"u_dirstuck": 'Nettleseren din fikk ikke tilgang til å lese følgende {0} filer/mapper, så de blir hoppet over:',
"u_etadone": 'Ferdig ({0}, {1} filer)',
"u_etaprep": '(forbereder opplastning)',
"u_hashdone": 'befaring ferdig',
"u_hashing": 'les',
"u_hs": 'serveren tenker...',
"u_dupdefer": "duplikat; vil bli håndtert til slutt",
"u_actx": "klikk her for å forhindre tap av<br />ytelse ved bytte til andre vinduer/faner",
"u_fixed": "OK!&nbsp; Løste seg 👍",
"u_cuerr": "kunne ikke laste opp del {0} av {1};\nsikkert greit, fortsetter\n\nfil: {2}",
"u_cuerr2": "server nektet opplastningen (del {0} av {1});\nprøver igjen senere\n\nfil: {2}\n\nerror ",
@@ -1162,10 +1228,18 @@ var Ls = {
"m_ok": "确定",
"m_ng": "取消",
"ht_s": "",
"ht_m": "",
"ht_h": "",
"ht_d": "天",
"enable": "启用",
"danger": "危险",
"clipped": "已复制到剪贴板",
"ht_s1": "秒",
"ht_s2": "秒",
"ht_m1": "分",
"ht_m2": "分",
"ht_h1": "时",
"ht_h2": "时",
"ht_d1": "天",
"ht_d2": "天",
"ht_and": " 和 ",
"goh": "控制面板",
@@ -1242,6 +1316,15 @@ var Ls = {
"utl_stat": "状态",
"utl_prog": "进度",
// 保持简短:
"utl_404": "404",
"utl_err": "错误",
"utl_oserr": "OS错误",
"utl_found": "已找到",
"utl_defer": "延期",
"utl_yolo": "加速",
"utl_done": "完成",
"ul_flagblk": "文件已添加到队列</b><br>但另一个浏览器标签中有一个繁忙的 up2k<br>因此等待它完成",
"ul_btnlk": "服务器配置已将此开关锁定到此状态",
@@ -1267,11 +1350,13 @@ var Ls = {
"cl_hcancel": "列隐藏已取消",
"ct_grid": '网格视图',
"ct_ttips": '◔ ◡ ◔"> 工具提示',
"ct_thumb": '在网格视图中,切换图标或缩略图$N快捷键: T">🖼️ 缩略图',
"ct_csel": '在网格视图中使用 CTRL 和 SHIFT 进行文件选择">CTRL',
"ct_ihop": '当图像查看器关闭时,滚动到最后查看的文件">滚动',
"ct_dots": '显示隐藏文件(如果服务器允许)">隐藏文件',
"ct_dir1st": '在文件之前排序文件夹">📁 排序',
"ct_nsort": '正确排序以数字开头的文件名">数字排序', //m
"ct_readme": '在文件夹列表中显示 README.md">📜 readme',
"ct_idxh": '显示 index.html 代替文件夹列表">htm',
"ct_sbars": '显示滚动条">⟊',
@@ -1398,7 +1483,8 @@ var Ls = {
"fs_pname": "链接名称可选;如果为空则随机",
"fs_tsrc": "共享的文件或文件夹",
"fs_ppwd": "密码可选",
"fs_ok": "<h6>分享链接已创建</h6>\n按 <code>Enter/OK</code> 复制到剪贴板\n按 <code>ESC/Cancel</code> 关闭\n\n",
"fs_w8": "正在创建文件共享...",
"fs_ok": "按 <code>Enter/OK</code> 复制到剪贴板\n按 <code>ESC/Cancel</code> 关闭",
"frt_dec": "可能修复一些损坏的文件名\">url-decode",
"frt_rst": "将修改后的文件名重置为原始文件名\">↺ 重置",
@@ -1408,6 +1494,8 @@ var Ls = {
"fr_case": "区分大小写的正则表达式\">case",
"fr_win": "Windows 安全名称;将 <code>&lt;&gt;:&quot;\\|?*</code> 替换为日文全角字符\">win",
"fr_slash": "将 <code>/</code> 替换为不会导致新文件夹创建的字符\">不使用 /",
"fr_re": "正则表达式搜索模式应用于原始文件名;$N可以在下面的格式字段中引用捕获组如&lt;code&gt;(1)&lt;/code&gt;和&lt;code&gt;(2)&lt;/code&gt;等等。",
"fr_fmt": "受到 foobar2000 的启发:$N&lt;code&gt;(title)&lt;/code&gt; 被歌曲名称替换,$N&lt;code&gt;[(artist) - ](title)&lt;/code&gt; 仅当歌曲艺术家不为空时才包含&lt;code&gt;[此]&lt;/code&gt;部分$N&lt;code&gt;$lpad((tn),2,0)&lt;/code&gt; 将曲目编号填充为 2 位数字",
"fr_pdel": "删除",
"fr_pnew": "另存为",
"fr_pname": "为你的新预设提供一个名称",
@@ -1417,7 +1505,7 @@ var Ls = {
"fr_tags": "选定文件的标签(只读,仅供参考):",
"fr_busy": "正在重命名 {0} 项...\n\n{1}",
"fr_efail": "重命名失败:\n",
"fr_nchg": "{0} 个新名称由于 <code>win</code> 和/或 <code>ikke /</code> 被更改\n\n确定继续使用这些更改的新名称",
"fr_nchg": "{0} 个新名称由于 <code>win</code> 和/或 <code>不使用 /</code> 被更改\n\n确定继续使用这些更改的新名称",
"fd_ok": "删除成功",
"fd_err": "删除失败:\n",
@@ -1467,7 +1555,8 @@ var Ls = {
"gt_c1": "截断文件名更多(显示更少)",
"gt_c2": "截断文件名更少(显示更多)",
"sm_prev": "以下是来自先前查询的搜索结果:\n ",
"sm_w8": "正在搜索...",
"sm_prev": "上次查询的搜索结果:\n ",
"sl_close": "关闭搜索结果",
"sl_hits": "显示 {0} 个结果",
"sl_moar": "加载更多",
@@ -1539,16 +1628,20 @@ var Ls = {
"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+",
"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_gotpot": '切换到简化UI以提高上传速度\n\n随时可以不同意并切换回去',
"u_pott": "<p>个文件: &nbsp; <b>{0}</b> 已完成, &nbsp; <b>{1}</b> 失败, &nbsp; <b>{2}</b> 正在处理, &nbsp; <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_ewrite": '你对这个文件夹没有写入权限',
@@ -1561,10 +1654,16 @@ var Ls = {
"u_up_life": "此上传将在 {0} 后从服务器删除",
"u_asku": '将这些 {0} 个文件上传到 <code>{1}</code>',
"u_unpt": "你可以使用左上角的 🧯 撤销/删除此上传",
"u_bigtab": '将显示 {0} 个文件,可能会导致您的浏览器崩溃。您确定吗?',
"u_scan": '正在扫描文件...',
"u_dirstuck": '您的浏览器无法访问以下 {0} 个文件/文件夹,它们将被跳过:',
"u_etadone": '完成 ({0}, {1} 个文件)',
"u_etaprep": '(准备上传)',
"u_hashdone": '哈希完成',
"u_hashing": '哈希',
"u_hs": '正在等待服务器...',
"u_dupdefer": "这是一个重复文件。它将在所有其他文件上传后进行处理",
"u_actx": "单击此文本以防止切换到其他窗口/选项卡时性能下降",
"u_fixed": "好!&nbsp;已修复 👍",
"u_cuerr": "上传块 {0} 的 {1} 失败;\n可能无害继续中\n\n文件{2}",
"u_cuerr2": "服务器拒绝上传(块 {0} 的 {1}\n稍后重试\n\n文件{2}\n\n错误 ",
@@ -1692,7 +1791,7 @@ ebi('widget').innerHTML = (
' <canvas id="barbuf"></canvas>' +
'</div>' +
'<div id="np_inf">' +
' <img id="np_img"></span>' +
' <img id="np_img" />' +
' <span id="np_url"></span>' +
' <span id="np_circle"></span>' +
' <span id="np_album"></span>' +
@@ -1810,13 +1909,14 @@ ebi('op_cfg').innerHTML = (
'<div>\n' +
' <h3>' + L.cl_opts + '</h3>\n' +
' <div>\n' +
' <a id="tooltips" class="tgl btn" href="#" tt="◔ ◡ ◔"> tooltips</a>\n' +
' <a id="tooltips" class="tgl btn" href="#" tt="' + L.ct_ttips + '</a>\n' +
' <a id="griden" class="tgl btn" href="#" tt="' + L.wt_grid + '">' + L.ct_grid + '</a>\n' +
' <a id="thumbs" class="tgl btn" href="#" tt="' + L.ct_thumb + '</a>\n' +
' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '</a>\n' +
' <a id="ihop" class="tgl btn" href="#" tt="' + L.ct_ihop + '</a>\n' +
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '</a>\n' +
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '</a>\n' +
' <a id="nsort" class="tgl btn" href="#" tt="' + L.ct_nsort + '</a>\n' +
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '</a>\n' +
' <a id="idxh" class="tgl btn" href="#" tt="' + L.ct_idxh + '</a>\n' +
' <a id="sbars" class="tgl btn" href="#" tt="' + L.ct_sbars + '</a>\n' +
@@ -2587,7 +2687,7 @@ var widget = (function () {
m += '[' + cv + s2ms(mp.au.currentTime) + ck + '/' + cv + s2ms(mp.au.duration) + ck + ']';
cliptxt(m, function () {
toast.ok(1, 'copied to clipboard', null, 'top');
toast.ok(1, L.clipped, null, 'top');
});
};
r.set(sread('au_open') == 1);
@@ -3607,7 +3707,7 @@ var afilt = (function () {
}
var html = ['<table><tr><td rowspan="4">',
'<a id="au_eq" class="tgl btn" href="#" tt="' + L.mt_eq + '">enable</a></td>'],
'<a id="au_eq" class="tgl btn" href="#" tt="' + L.mt_eq + '">' + L.enable + '</a></td>'],
h2 = [], h3 = [], h4 = [];
var vs = [];
@@ -3637,7 +3737,7 @@ var afilt = (function () {
h2 = [];
html = ['<table><tr><td rowspan="2">',
'<a id="au_drc" class="tgl btn" href="#" tt="' + L.mt_drc + '">enable</a></td>'];
'<a id="au_drc" class="tgl btn" href="#" tt="' + L.mt_drc + '">' + L.enable + '</a></td>'];
for (var a = 0; a < r.drch.length; a++) {
html.push('<td>' + r.drch[a] + '</td>');
@@ -3951,11 +4051,12 @@ function eval_hash() {
break;
}
if (mi && img_re.exec(og_fn))
hash0 = '#g' + mi.id;
var ch = !mi ? '' :
img_re.exec(og_fn) ? 'g' :
ebi('a' + mi.id) ? 'a' :
'';
if (ebi('a' + mi.id))
hash0 = '#a' + mi.id;
hash0 = ch ? ('#' + ch + mi.id) : '';
}
var v = hash0;
@@ -3996,14 +4097,14 @@ function eval_hash() {
}
}
if (v.indexOf('#q=') === 0) {
if (v.startsWith('#q=')) {
goto('search');
var i = ebi('q_raw');
i.value = uricom_dec(v.slice(3));
return i.onkeydown({ 'key': 'Enter' });
}
if (v.indexOf('#v=') === 0) {
if (v.startsWith('#v=')) {
goto(v.slice(3));
return;
}
@@ -4090,6 +4191,9 @@ function sortfiles(nodes) {
var sopts = jread('fsort', jcp(dsort)),
dir1st = sread('dir1st') !== '0';
var collator = sread('nsort') != 1 ? null :
new Intl.Collator([], {numeric: true});
try {
var is_srch = false;
if (nodes[0]['rp']) {
@@ -4139,7 +4243,9 @@ function sortfiles(nodes) {
}
if (v2 === undefined) return 1 * rev;
var ret = rev * (typ == 'int' ? (v1 - v2) : (v1.localeCompare(v2)));
var ret = rev * (typ == 'int' ? (v1 - v2) : collator ?
collator.compare(v1, v2) : v1.localeCompare(v2));
if (ret === 0)
ret = onodes.indexOf(n1) - onodes.indexOf(n2);
@@ -4499,11 +4605,12 @@ var fileman = (function () {
return;
}
surl = surl.slice(15);
modal.confirm(L.fs_ok + esc(surl), function() {
var txt = esc(surl) + '<img class="b64" src="' + surl + '?qr" />';
modal.confirm(txt + L.fs_ok, function() {
cliptxt(surl, function () {
toast.ok(2, 'copied to clipboard');
toast.ok(2, L.clipped);
});
});
}, null);
}
sh_apply.onclick = function () {
@@ -4516,7 +4623,7 @@ var fileman = (function () {
plist.push(pbtns[a].textContent);
shui.style.display = 'none';
toast.inf(30, "creating share...");
toast.inf(30, L.fs_w8);
var body = {
"k": sh_k.value,
@@ -4537,13 +4644,13 @@ var fileman = (function () {
r.rename = function (e) {
ev(e);
if (clgot(bren, 'hide'))
return toast.err(3, L.fr_eperm);
var sel = msel.getsel();
if (!sel.length)
return toast.err(3, L.fr_emore);
if (clgot(bren, 'hide'))
return toast.err(3, L.fr_eperm);
var f = [],
base = vsplit(sel[0].vp)[0],
mkeys;
@@ -4604,8 +4711,8 @@ var fileman = (function () {
'<a id="rn_slash" class="tgl btn" href="#" tt="' + L.fr_slash + '</a>',
'</div>',
'<div id="rn_vadv"><table>',
'<tr><td>regex</td><td><input type="text" id="rn_re" ' + NOAC + ' tt="regex search pattern to apply to original filenames; capturing groups can be referenced in the format field below like &lt;code&gt;(1)&lt;/code&gt; and &lt;code&gt;(2)&lt;/code&gt; and so on" placeholder="^[0-9]+[\\. ]+(.*) - (.*)" /></td></tr>',
'<tr><td>format</td><td><input type="text" id="rn_fmt" ' + NOAC + ' tt="inspired by foobar2000:$N&lt;code&gt;(title)&lt;/code&gt; is replaced by song title,$N&lt;code&gt;[(artist) - ](title)&lt;/code&gt; skips the first part if artist is blank$N&lt;code&gt;$lpad((tn),2,0)&lt;/code&gt; pads tracknumber to 2 digits" placeholder="[(artist) - ](title).(ext)" /></td></tr>',
'<tr><td>regex</td><td><input type="text" id="rn_re" ' + NOAC + ' tt="' + L.fr_re + '" placeholder="^[0-9]+[\\. ]+(.*) - (.*)" /></td></tr>',
'<tr><td>format</td><td><input type="text" id="rn_fmt" ' + NOAC + ' tt="' + L.fr_fmt + '" placeholder="[(artist) - ](title).(ext)" /></td></tr>',
'<tr><td>preset</td><td><select id="rn_pre"></select>',
'<button id="rn_pdel">❌ ' + L.fr_pdel + '</button>',
'<button id="rn_pnew">💾 ' + L.fr_pnew + '</button>',
@@ -4613,7 +4720,9 @@ var fileman = (function () {
'</table></div>'
]);
var cheap = f.length > 500;
var cheap = f.length > 500,
t_rst = L.frt_rst.split('>').pop();
if (sel.length == 1)
html.push(
'<div><table id="rn_f">\n' +
@@ -4628,7 +4737,7 @@ var fileman = (function () {
'<tr><td>' +
(cheap ? '</td>' :
'<button class="rn_dec" n="' + a + '">decode</button>' +
'<button class="rn_reset" n="' + a + '">↺ reset</button></td>') +
'<button class="rn_reset" n="' + a + '">' + t_rst + '</button></td>') +
'<td><input type="text" id="rn_new" n="' + a + '" /></td>' +
'<td><input type="text" id="rn_old" n="' + a + '" readonly /></td></tr>');
}
@@ -4844,9 +4953,6 @@ var fileman = (function () {
r.delete = function (e) {
ev(e);
if (clgot(bdel, 'hide'))
return toast.err(3, L.fd_eperm);
var sel = msel.getsel(),
vps = [];
@@ -4856,6 +4962,9 @@ var fileman = (function () {
if (!sel.length)
return toast.err(3, L.fd_emore);
if (clgot(bdel, 'hide'))
return toast.err(3, L.fd_eperm);
function deleter(err) {
var xhr = new XHR(),
vp = vps.shift();
@@ -4886,21 +4995,21 @@ var fileman = (function () {
deleter();
}
modal.confirm('<h6 style="color:#900">DANGER</h6>\n<b>' + L.fd_warn1.format(vps.length) + '</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () {
modal.confirm('<h6 style="color:#900">' + L.danger + '</h6>\n<b>' + L.fd_warn1.format(vps.length) + '</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () {
modal.confirm(L.fd_warn2, deleter, null);
}, null);
};
r.cut = function (e) {
ev(e);
if (clgot(bcut, 'hide'))
return toast.err(3, L.fc_eperm);
var sel = msel.getsel(),
vps = [];
if (!sel.length)
toast.err(3, L.fc_emore);
return toast.err(3, L.fc_emore);
if (clgot(bcut, 'hide'))
return toast.err(3, L.fc_eperm);
var els = [], griden = thegrid.en;
for (var a = 0; a < sel.length; a++) {
@@ -5008,12 +5117,12 @@ var fileman = (function () {
};
r.paste = function () {
if (clgot(bpst, 'hide'))
return toast.err(3, L.fp_eperm);
if (!r.clip.length)
return toast.err(5, L.fp_ecut);
if (clgot(bpst, 'hide'))
return toast.err(3, L.fp_eperm);
var req = [],
exists = [],
indir = [],
@@ -5161,6 +5270,12 @@ var showfile = (function () {
return srch.split(/[?&]doc=/)[1].split('&')[0];
};
if (window.og_fn) {
var ext = og_fn.split(/\./g).pop();
if (r.map['.' + ext])
hist_replace(get_evpath() + '?doc=' + og_fn);
}
window.Prism = { 'manual': true };
var em = QS('#bdoc>pre');
if (em)
@@ -5208,8 +5323,12 @@ var showfile = (function () {
r.files.push({ 'id': link.id, 'name': uricom_dec(fn) });
var td = ebi(link.id).closest('tr').getElementsByTagName('td')[0];
var ah = ebi(link.id),
td = ah.closest('tr').getElementsByTagName('td')[0];
if (ah.textContent.endsWith('/'))
continue;
if (lang == 'ts' || (lang == 'md' && td.textContent != '-'))
continue;
@@ -5577,10 +5696,7 @@ var thegrid = (function () {
swrite('gridln', r.ln);
setTimeout(r.tippen, 20);
}
try {
document.documentElement.style.setProperty('--grid-ln', r.ln);
}
catch (ex) { }
setcvar('--grid-ln', r.ln);
}
setln();
@@ -5590,10 +5706,7 @@ var thegrid = (function () {
swrite('gridsz', r.sz);
setTimeout(r.tippen, 20);
}
try {
document.documentElement.style.setProperty('--grid-sz', r.sz + 'em');
}
catch (ex) { }
setcvar('--grid-sz', r.sz + 'em');
aligngriditems();
}
setsz();
@@ -6502,7 +6615,7 @@ var ahotkeys = function (e) {
function do_search() {
search_in_progress = Date.now();
srch_msg(false, "searching...");
srch_msg(false, L.sm_w8);
clearTimeout(search_timeout);
var xhr = new XHR();
@@ -6656,10 +6769,7 @@ var filecolwidth = (function () {
return;
lastwidth = w;
try {
document.documentElement.style.setProperty('--file-td-w', w + 'em');
}
catch (ex) { }
setcvar('--file-td-w', w + 'em');
}
})();
onresize100.add(filecolwidth, true);
@@ -6680,6 +6790,9 @@ var treectl = (function () {
mentered = null,
treesz = clamp(icfg_get('treesz', 16), 10, 50);
var resort = function () {
treectl.gentab(get_evpath(), treectl.lsc);
};
bcfg_bind(r, 'ireadme', 'ireadme', true);
bcfg_bind(r, 'idxh', 'idxh', idxh, setidxh);
bcfg_bind(r, 'dyn', 'dyntree', true, onresize);
@@ -6690,9 +6803,8 @@ var treectl = (function () {
xhr.open('GET', SR + '/?setck=dots=' + (v ? 'y' : ''), true);
xhr.send();
});
bcfg_bind(r, 'dir1st', 'dir1st', true, function (v) {
treectl.gentab(get_evpath(), treectl.lsc);
});
bcfg_bind(r, 'nsort', 'nsort', false, resort);
bcfg_bind(r, 'dir1st', 'dir1st', true, resort);
setwrap(bcfg_bind(r, 'wtree', 'wraptree', true, setwrap));
setwrap(bcfg_bind(r, 'parpane', 'parpane', true, onscroll));
bcfg_bind(r, 'htree', 'hovertree', false, reload_tree);
@@ -6901,10 +7013,7 @@ var treectl = (function () {
w = iw + 'em',
w2 = (iw + 2) + 'em';
try {
document.documentElement.style.setProperty('--nav-sz', w);
}
catch (ex) { }
setcvar('--nav-sz', w);
ebi('tree').style.width = w;
ebi('wrap').style.marginLeft = w2;
onscroll();
@@ -7236,6 +7345,9 @@ var treectl = (function () {
var lg0 = res.logues ? res.logues[0] || "" : "",
lg1 = res.logues ? res.logues[1] || "" : "",
mds = res.readmes && treectl.ireadme,
md0 = mds ? res.readmes[0] || "" : "",
md1 = mds ? res.readmes[1] || "" : "",
dirchg = get_evpath() != cdir;
if (lg1 === Ls.eng.f_empty)
@@ -7245,9 +7357,14 @@ var treectl = (function () {
if (dirchg)
sandbox(ebi('epi'), sb_lg, '', lg1);
clmod(ebi('pro'), 'mdo');
clmod(ebi('epi'), 'mdo');
if (res.readme && treectl.ireadme)
show_readme(res.readme);
if (md0)
show_readme(md0, 0);
if (md1)
show_readme(md1, 1);
else if (!dirchg)
sandbox(ebi('epi'), sb_lg, '', lg1);
@@ -7694,7 +7811,7 @@ function apply_perms(res) {
ebi('acc_info').innerHTML = '<span id="srv_info2"><span>' + srvinf +
'</span></span><span' + aclass + axs + L.access + '</span>' + (acct != '*' ?
'<a href="' + SR + '/?pw=x">' + (window.is_idp ? '' : L.logout) + acct + '</a>' :
'<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>');
var o = QSA('#ops>a[data-perm]');
@@ -8768,14 +8885,17 @@ function set_tabindex() {
}
function show_readme(md) {
if (!treectl.ireadme)
return sandbox(ebi('epi'), '', '', 'a');
function show_readme(md, n) {
var tgt = ebi(n ? 'epi' : 'pro');
show_md(md, 'README.md', ebi('epi'));
if (!treectl.ireadme)
return sandbox(tgt, '', '', 'a');
show_md(md, n ? 'README.md' : 'PREADME.md', tgt);
}
if (readme)
show_readme(readme);
for (var a = 0; a < readmes.length; a++)
if (readmes[a])
show_readme(readmes[a], a);
function sandbox(tgt, rules, cls, html) {
@@ -9112,7 +9232,7 @@ var unpost = (function () {
ev(e);
var txt = linklist();
cliptxt(txt + '\n', function () {
toast.inf(5, txt.split('\n').length + ' links copied to clipboard');
toast.inf(5, L.un_clip.format(txt.split('\n').length));
});
};

View File

@@ -27,7 +27,7 @@ a {
padding: .2em .6em;
margin: 0 .3em;
}
td a {
#wrap td a {
margin: 0;
}
#w {
@@ -53,12 +53,13 @@ th {
position: sticky;
background: #f7f7f7;
}
td, th {
#wrap td,
#wrap th {
padding: .3em .6em;
text-align: left;
white-space: nowrap;
}
td+td+td+td+td+td+td+td {
#wrap td+td+td+td+td+td+td+td {
font-family: var(--font-mono), monospace, monospace;
}

View File

@@ -22,8 +22,8 @@
<span>min/hrs = time left</span>
<table id="tab"><thead><tr>
<th>delete</th>
<th>sharekey</th>
<th>delete</th>
<th>pw</th>
<th>source</th>
<th>axs</th>
@@ -37,10 +37,13 @@
</tr></thead><tbody>
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
<tr>
<td>
<a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a>
<a href="{{ r }}{{ shr }}{{ k }}">{{ k }}</a>
</td>
<td><a href="#" k="{{ k }}">delete</a></td>
<td><a href="{{ r }}{{ shr }}{{ k }}">{{ k }}</a></td>
<td>{{ pw }}</td>
<td><a href="{{ r }}/{{ vp|e }}">{{ vp|e }}</a></td>
<td>{{ "yes" if pw else "--" }}</td>
<td><a href="{{ r }}/{{ vp|e }}">/{{ vp|e }}</a></td>
<td>{{ pr }}</td>
<td>{{ st }}</td>
<td>{{ un|e }}</td>

View File

@@ -12,7 +12,7 @@ function rm() {
}
function bump() {
var k = this.closest('tr').getElementsByTagName('a')[0].getAttribute('k'),
var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'),
u = SR + shr + uricom_enc(k) + '?eshare=' + this.value,
xhr = new XHR();
@@ -28,14 +28,36 @@ function cb() {
document.location = '?shares';
}
function qr(e) {
ev(e);
var href = this.href,
pw = this.closest('tr').cells[2].textContent;
if (pw.indexOf('yes') < 0)
return showqr(href);
modal.prompt("if you want to bypass the password protection by\nembedding the password into the qr-code, then\ntype the password now, otherwise leave this empty", "", function (v) {
if (v)
href += "&pw=" + v;
showqr(href);
});
}
function showqr(href) {
var vhref = href.replace('?qr&', '?').replace('?qr', '');
modal.alert(esc(vhref) + '<img class="b64" src="' + href + '" />');
}
(function() {
var tab = ebi('tab').tBodies[0],
tr = Array.prototype.slice.call(tab.rows, 0);
var buf = [];
for (var a = 0; a < tr.length; a++)
for (var a = 0; a < tr.length; a++) {
tr[a].cells[0].getElementsByTagName('a')[0].onclick = qr;
for (var b = 7; b < 9; b++)
buf.push(parseInt(tr[a].cells[b].innerHTML));
}
var ibuf = 0;
for (var a = 0; a < tr.length; a++)

View File

@@ -53,7 +53,7 @@ a.r {
border-color: #c7a;
}
a.g {
color: #2b0;
color: #0a0;
border-color: #3a0;
box-shadow: 0 .3em 1em #4c0;
}
@@ -152,11 +152,13 @@ pre b,
code b {
color: #000;
font-weight: normal;
text-shadow: 0 0 .2em #0f0;
text-shadow: 0 0 .2em #3f3;
border-bottom: 1px solid #090;
}
html.z pre b,
html.z code b {
color: #fff;
border-bottom: 1px solid #9f9;
}

View File

@@ -32,6 +32,18 @@
</div>
{%- endif %}
{%- if ups %}
<h1 id="aa">incoming files:</h1>
<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 %}
<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 %}
</tbody>
</table>
{%- endif %}
{%- if avol %}
<h1>admin panel:</h1>
<table><tr><td> <!-- hehehe -->

View File

@@ -9,7 +9,7 @@ var Ls = {
"e2": "leser inn konfigurasjonsfiler på nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer$N$Nmerk: endringer i globale parametere$Nkrever en full restart for å ta gjenge",
"f1": "du kan betrakte:",
"g1": "du kan laste opp til:",
"cc1": "brytere og sånt",
"cc1": "brytere og sånt:",
"h1": "skru av k304",
"i1": "skru på k304",
"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",
@@ -25,20 +25,21 @@ var Ls = {
"t1": "handling",
"u2": "tid siden noen sist skrev til serveren$N( opplastning / navneendring / ... )$N$N17d = 17 dager$N1h23 = 1 time 23 minutter$N4m56 = 4 minuter 56 sekunder",
"v1": "koble til",
"v2": "bruk denne serveren som en lokal harddisk$N$NADVARSEL: kommer til å vise passordet ditt!",
"v2": "bruk denne serveren som en lokal harddisk",
"w1": "bytt til https",
"x1": "bytt passord",
"y1": "dine delinger",
"z1": "lås opp område",
"z1": "lås opp område:",
"ta1": "du må skrive et nytt passord først",
"ta2": "gjenta for å bekrefte nytt passord:",
"ta3": "fant en skrivefeil; vennligst prøv igjen",
"aa1": "innkommende:",
},
"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",
"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$N$NWARNING: this will show your password!",
"v2": "use this server as a local HDD",
"ta1": "fill in your new password first",
"ta2": "repeat to confirm new password:",
"ta3": "found a typo; please try again",
@@ -70,7 +71,7 @@ var Ls = {
"t1": "操作",
"u2": "自上次服务器写入的时间$N( 上传 / 重命名 / ... )$N$N17d = 17 天$N1h23 = 1 小时 23 分钟$N4m56 = 4 分钟 56 秒",
"v1": "连接",
"v2": "将此服务器用作本地硬盘$N$N警告这将显示你的密码",
"v2": "将此服务器用作本地硬盘",
"w1": "切换到 https",
"x1": "更改密码",
"y1": "你的分享",
@@ -78,6 +79,7 @@ var Ls = {
"ta1": "请先输入新密码",
"ta2": "重复以确认新密码:",
"ta3": "发现拼写错误;请重试",
"aa1": "正在接收的文件:", //m
}
};

View File

@@ -192,6 +192,7 @@
<h1>partyfuse</h1>
<p>
<a href="{{ r }}/.cpr/a/partyfuse.py">partyfuse.py</a> -- fast, read-only,
needs <a href="{{ r }}/.cpr/deps/fuse.py">fuse.py</a> in the same folder,
<span class="os win">needs <a href="https://winfsp.dev/rel/">winfsp</a></span>
<span class="os lin">doesn't need root</span>
</p>
@@ -208,7 +209,6 @@
{% if args.smb %}
<h1>SMB / CIFS</h1>
<em><a href="https://github.com/SecureAuthCorp/impacket/issues/1433">bug:</a> max ~300 files in each folder</em>
<div class="os win">
<pre>

View File

@@ -69,6 +69,18 @@ html {
top: 2em;
bottom: unset;
}
#toastt {
position: absolute;
height: 1px;
top: 1px;
right: 1%;
width: 99%;
animation: toastt var(--tmtime) steps(var(--tmstep)) forwards;
transform-origin: right;
}
@keyframes toastt {
to {transform: scaleX(0)}
}
#toast a {
color: inherit;
text-shadow: inherit;
@@ -130,6 +142,9 @@ html {
#toast.inf #toastc {
background: #0be;
}
#toast.inf #toastt {
background: #8ef;
}
#toast.ok {
background: #380;
border-color: #8e4;
@@ -137,6 +152,9 @@ html {
#toast.ok #toastc {
background: #8e4;
}
#toast.ok #toastt {
background: #cf9;
}
#toast.warn {
background: #960;
border-color: #fc0;
@@ -144,6 +162,9 @@ html {
#toast.warn #toastc {
background: #fc0;
}
#toast.warn #toastt {
background: #fe9;
}
#toast.err {
background: #900;
border-color: #d06;
@@ -151,6 +172,9 @@ html {
#toast.err #toastc {
background: #d06;
}
#toast.err #toastt {
background: #f9c;
}
#toast code {
padding: 0 .2em;
background: rgba(0,0,0,0.2);
@@ -293,6 +317,12 @@ html.y #tth {
#modalc a {
color: #07b;
}
#modalc .b64 {
display: block;
margin: .1em auto;
width: 60%;
height: 60%;
}
#modalb {
position: sticky;
text-align: right;

View File

@@ -152,12 +152,13 @@ function U2pvis(act, btns, uc, st) {
r.mod0 = null;
var markup = {
'404': '<span class="err">404</span>',
'ERROR': '<span class="err">ERROR</span>',
'OS-error': '<span class="err">OS-error</span>',
'found': '<span class="inf">found</span>',
'YOLO': '<span class="inf">YOLO</span>',
'done': '<span class="ok">done</span>',
'404': '<span class="err">' + L.utl_404 + '</span>',
'ERROR': '<span class="err">' + L.utl_err + '</span>',
'OS-error': '<span class="err">' + L.utl_oserr + '</span>',
'found': '<span class="inf">' + L.utl_found + '</span>',
'defer': '<span class="inf">' + L.utl_defer + '</span>',
'YOLO': '<span class="inf">' + L.utl_yolo + '</span>',
'done': '<span class="ok">' + L.utl_done + '</span>',
};
r.addfile = function (entry, sz, draw) {
@@ -445,9 +446,7 @@ function U2pvis(act, btns, uc, st) {
return;
r.npotato = 0;
var html = [
"<p>files: &nbsp; <b>{0}</b> finished, &nbsp; <b>{1}</b> failed, &nbsp; <b>{2}</b> busy, &nbsp; <b>{3}</b> queued</p>".format(
r.ctr.ok, r.ctr.ng, r.ctr.bz, r.ctr.q)];
var html = [L.u_pott.format(r.ctr.ok, r.ctr.ng, r.ctr.bz, r.ctr.q)];
while (r.head < r.tab.length && has(["ok", "ng"], r.tab[r.head].in))
r.head++;
@@ -602,7 +601,7 @@ function U2pvis(act, btns, uc, st) {
if (nf < 9000)
return go();
modal.confirm('about to show ' + nf + ' files\n\nthis may crash your browser, are you sure?', go, null);
modal.confirm(L.u_bigtab.format(nf), go, null);
};
}
@@ -1037,7 +1036,7 @@ function up2k_init(subtle) {
}
catch (ex) {
document.body.ondragenter = document.body.ondragleave = document.body.ondragover = null;
return modal.alert('your browser does not support drag-and-drop uploading');
return modal.alert(L.u_nodrop);
}
if (btn)
return;
@@ -1104,7 +1103,7 @@ function up2k_init(subtle) {
}
if (!good_files.length && bad_files.length)
return toast.err(30, "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead");
return toast.err(30, L.u_notdir);
return read_dirs(null, [], [], good_files, nil_files, bad_files);
}
@@ -1122,7 +1121,7 @@ function up2k_init(subtle) {
if (err)
return modal.alert('sorry, ' + err);
toast.inf(0, 'Scanning files...');
toast.inf(0, L.u_scan);
if ((dz == 'up_dz' && uc.fsearch) || (dz == 'srch_dz' && !uc.fsearch))
tgl_fsearch();
@@ -1210,7 +1209,7 @@ function up2k_init(subtle) {
match = false;
if (match) {
var msg = ['directory iterator got stuck trying to access the following {0} items; will skip:<ul>'.format(missing.length)];
var msg = [L.u_dirstuck.format(missing.length) + '<ul>'];
for (var a = 0; a < Math.min(20, missing.length); a++)
msg.push('<li>' + esc(missing[a]) + '</li>');
@@ -1281,7 +1280,7 @@ function up2k_init(subtle) {
}
function gotallfiles(good_files, nil_files, bad_files) {
if (toast.txt == 'Scanning files...')
if (toast.txt == L.u_scan)
toast.hide();
if (uc.fsearch && !uc.turbo)
@@ -1437,7 +1436,7 @@ function up2k_init(subtle) {
if (!actx || actx.state != 'suspended' || toast.visible)
return;
toast.warn(30, "<div onclick=\"start_actx();toast.inf(3,'thanks!')\">please click this text to<br />unlock full upload speed</div>");
toast.warn(30, "<div onclick=\"start_actx();toast.inf(3,'thanks!')\">" + L.u_actx + "</div>");
}, 500);
}
@@ -1479,7 +1478,7 @@ function up2k_init(subtle) {
ev(e);
var txt = linklist();
cliptxt(txt + '\n', function () {
toast.inf(5, txt.split('\n').length + ' links copied to clipboard');
toast.inf(5, un_clip.format(txt.split('\n').length));
});
};
@@ -1746,14 +1745,6 @@ function up2k_init(subtle) {
var mou_ikkai = false;
if (st.busy.handshake.length &&
st.busy.handshake[0].t_busied < now - 30 * 1000
) {
console.log("retrying stuck handshake");
var t = st.busy.handshake.shift();
st.todo.handshake.unshift(t);
}
var nprev = -1;
for (var a = 0; a < st.todo.upload.length; a++) {
var nf = st.todo.upload[a].nfile;
@@ -2255,6 +2246,9 @@ function up2k_init(subtle) {
if (keepalive)
console.log("sending keepalive handshake", t.name, t);
if (!t.srch && !t.t_handshake)
pvis.seth(t.n, 2, L.u_hs);
var xhr = new XMLHttpRequest();
xhr.onerror = xhr.ontimeout = function () {
if (t.t_busied != me) // t.done ok
@@ -2281,7 +2275,8 @@ function up2k_init(subtle) {
apop(st.busy.handshake, t);
st.todo.handshake.unshift(t);
t.cooldown = Date.now() + 5000 + Math.floor(Math.random() * 3000);
return toast.err(0, 'Handshake error; will retry...\n\n' + L.badreply + ':\n\n' + unpre(xhr.responseText));
var txt = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit;
return toast.err(0, txt + '\n\n' + L.badreply + ':\n\n' + unpre(xhr.responseText));
}
t.t_handshake = Date.now();
@@ -2460,6 +2455,7 @@ function up2k_init(subtle) {
pvis.seth(t.n, 2, L.u_ehstmp, t);
var err = "",
cls = "ERROR",
rsp = unpre(xhr.responseText),
ofs = rsp.lastIndexOf('\nURL: ');
@@ -2489,6 +2485,8 @@ function up2k_init(subtle) {
if (!t.rechecks && (err_pend || err_srcb)) {
t.rechecks = 0;
t.want_recheck = true;
err = L.u_dupdefer;
cls = 'defer';
}
}
if (rsp.indexOf('server HDD is full') + 1)
@@ -2498,7 +2496,7 @@ function up2k_init(subtle) {
if (!t.t_uploading)
st.bytes.finished += t.size;
pvis.seth(t.n, 1, "ERROR");
pvis.seth(t.n, 1, cls);
pvis.seth(t.n, 2, err);
pvis.move(t.n, 'ng');
@@ -2530,7 +2528,8 @@ function up2k_init(subtle) {
xhr.open('POST', t.purl, true);
xhr.responseType = 'text';
xhr.timeout = 42000;
xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 :
(t.size / (1048 * 20))); // safededup 20M/s hdd
xhr.send(JSON.stringify(req));
}
@@ -2625,7 +2624,7 @@ function up2k_init(subtle) {
t.nojoin = t.nojoin || t.postlist.length;
console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t);
if (!toast.visible && st.todo.upload.length < 4)
toast.msg(10, L.u_cbusy);
toast.inf(10, L.u_cbusy);
}
else {
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)", "warn", t);

View File

@@ -535,6 +535,14 @@ function clgot(el, cls) {
}
function setcvar(k, v) {
try {
document.documentElement.style.setProperty(k, v);
}
catch (e) { }
}
var ANIM = true;
try {
var mq = window.matchMedia('(prefers-reduced-motion: reduce)');
@@ -922,15 +930,18 @@ function shumantime(v, long) {
function lhumantime(v) {
var t = shumantime(v, 1),
tp = t.replace(/([a-z])/g, " $1 ").split(/ /g).slice(0, -1);
var t = shumantime(v, 1);
if (/[0-9]$/.exec(t))
t += 's';
var tp = t.replace(/([a-z])/g, " $1 ").split(/ /g).slice(0, -1);
if (!L || tp.length < 2 || tp[1].indexOf('$') + 1)
return t;
var ret = '';
for (var a = 0; a < tp.length; a += 2)
ret += tp[a] + ' ' + L['ht_' + tp[a + 1]].replace(tp[a] == 1 ? /!.*/ : /!/, '') + L.ht_and;
ret += tp[a] + ' ' + L['ht_' + tp[a + 1] + (tp[a]==1?1:2)] + L.ht_and;
return ret.slice(0, -L.ht_and.length);
}
@@ -1516,13 +1527,21 @@ var toast = (function () {
if (sec)
te = setTimeout(r.hide, sec * 1000);
if (same && delta < 1000)
var tb = ebi('toastt');
if (same && delta < 1000 && tb) {
tb.style.animation = 'none';
tb.offsetHeight;
tb.style.animation = null;
return;
}
if (txt.indexOf('<body>') + 1)
txt = txt.slice(0, txt.indexOf('<')) + ' [...]';
obj.innerHTML = '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';
setcvar('--tmtime', sec + 's');
setcvar('--tmstep', sec * 15);
obj.innerHTML = '<div id="toastt"></div><a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';
obj.className = cl;
sec += obj.offsetWidth;
obj.className += ' vis';

View File

@@ -1,3 +1,260 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-1011-2256 `v1.15.6` preadme
## 🧪 new features
* #105 files named `preadme.md` appear at the top of directory listings 1d68acf8
* entirely disable dedup with `--no-clone` / volflag `noclone` 3d7facd7 6b7ebdb7
* even if a file exists for sure on the server HDD, let the client continue uploading instead of reusing the existing data
* using this option "never" makes sense, unless you're using something like S3 Glacier storage where reading is really expensive but writing is cheap
## 🩹 bugfixes
* up2k jank after detecting a bitflip or network glitch 4a4ec88d
* instead of resuming the interrupted upload like it should, the upload client could get stuck or start over
* #104 support viewing dotfile documents when dotfiles are hidden 9ccd8bb3
* fix a buttload of typos 6adc778d 1e7697b5
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-1005-1803 `v1.15.5` pyz all the cores
## 🩹 bugfixes
* the pkgres / pyz changes in 1.15.4 broke multiprocessing c3985537
## 🔧 other changes
* pyz: drop easymde to save some bytes + make it a tiny bit faster
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-1004-2319 `v1.15.4` hermetic
## 🧪 new features
* [u2c](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) (commandline uploader):
* remove all dependencies; now entirely self-contained 9daeed92
* made it 3x faster for small files, 2x faster in general
* improve `-x` behavior to not traverse into excluded folders b9c5c7bb
* [partyfuse](https://github.com/9001/copyparty/tree/hovudstraum/bin#partyfusepy) (fuse client; mount a copyparty server as a local filesystem):
* 9x faster directory listings 03f0f994
* 4x faster downloads on high-latency connections 847a2bdc
* embed `fuse.py` (its only dependency) -- can be downloaded from the connect-page 44f2b63e
* support mounting nginx and iis servers too, not just copyparty c81e8984
* reduce ram usage down to 10% when running without `-e2d` 88a1c5ca
* does not affect servers with `-e2d` enabled (was already optimal)
* share folders as qr-codes e4542064
* when creating a share, you get a qr-code for quick access
* buttons in the shares controlpanel to reshow it, optionally with the password embedded into the qr-code
* #98 read embedded webdeps and templates with `pkg_resources`; thx @shizmob! a462a644 d866841c
* [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) now runs straight from the source file without unpacking anything to disk
* ...and is now much slower at returning resource GETs, but that is fine
* og / opengraph / discord embeds: support filekeys ae982006
* add option for natural sorting; thx @oshiteku! 9804f25d
* eyecandy timer bar on toasts 0dfe1d5b
* smb-server: impacket 0.12 is out! dc4d0d8e
* now *possible* to list folders with more than 400 files (it's REALLY slow)
## 🩹 bugfixes
* webdav:
* support `<allprop/>` in propfind dc157fa2
* list volumes when root is unmapped 480ac254
* previously, clients couldn't connect to the root of a copyparty server unless a volume existed at `/`
* #101 show `.prologue.html` and `.epilogue.html` in directory listings even if user cannot see hidden files 21be82ef
* #100 confusing toast when pressing F2 without selecting anything 2715ee6c
* fix prometheus metrics 678675a9
## 🔧 other changes
* #100 allow uploading `.prologue.html` and `.epilogue.html` 19a5985f
* #102 make translation easier when running in docker
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0916-0107 `v1.15.3` incoming eta
## 🧪 new features
![cpanel-upload-eta2-or8](https://github.com/user-attachments/assets/eb003bd4-da3c-4995-bf6e-3a8c1c1b26dd)
* incoming uploads (and their ETA) are shown in the controlpanel 609c5921 844194ee
* list total directory sizes 427597b6
* show the total size and number of files of each directory in listings
* makes browsing a bit slower (up to 30%) so can be disabled with `--no-dirsz`
* sizes are calculated during startup, so it requires `-e2dsa`
* file-uploads will recalculate the sizes immediately, but a full rescan is necessary to see changes caused by moves/deletes
* optimizations;
* reduce broker overhead when multiprocessing is disabled 4e75534e
* should reduce cpu usage by uploads, thumbnails, prometheus metrics
* reduce cpu usage from downloading thumbnails 7d64879b
## 🩹 bugfixes
* fix sqlite indexes d67e9cc5
* upload handshakes would get exponentially slow if a volume has more than 200'000 files
* reindex on startup can be 150x faster in some rare cases (same filename in MANY folders)
* the database is now around 10% larger (likely worst-case)
* misc ux: 58835b2b
* shares: show media tags
* html hydrator assumed a folder named `foo.txt` was a doc
* due to sessions, use `pwd` as password placeholder on services
## 🔧 other changes
* add [example](https://github.com/9001/copyparty/tree/hovudstraum/contrib#flameshotsh) for uploading screenshots from linux with flameshot 1c2acdc9
* [nginx example](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf): use unix-sockets for higher performance a5ce1032
* #97 chinese translation was improved, thx again @ultwcz 7a573caf
## 🗿 known issues
* prometheus metrics are busted
* **workaround:** disable monitoring of volume status with `--nos-vst`
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0909-2343 `v1.15.1` session
<img src="https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg" width="250" align="right"/>
blessed by ⑨, this release is [certified strong](https://github.com/user-attachments/assets/05459032-736c-4b9a-9ade-a0044461194a) ([artist](https://x.com/hcnone))
## new features
* login sessions b5405174
* a random session cookie is generated for each known user, replacing the previous plaintext login cookie
* the logout button will nuke the session on all clients where that user is logged in
* the sessions are stored in the database at `--ses-db`, default `~/.config/copyparty/sessions.db` (docker uses `/cfg/sessions.db` similar to the other runtime configs)
* if you run multiple copyparty instances, much like [shares](https://github.com/9001/copyparty#shares) and [user-changeable passwords](https://github.com/9001/copyparty#user-changeable-passwords) you'll want to keep a separate db for each instance
* can be mostly disabled with `--no-ses` when it turns out to be buggy
## bugfixes
* v1.13.8 broke the u2c `--ow` option to replace/overwrite files on the server during upload 6eee6015
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0908-1925 `v1.15.0` fill the drives
## recent important news
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
# upload deduplication now disabled by default
because many people found the behavior surprising. This also makes it easier to use copyparty together with other software, since there is no risk of damage to symlinks if there are no symlinks to damage
to enable deduplication, use either `--dedup` (old-default, symlink-based), or `--hardlink` (will use hardlinks when possible), or `--hardlink-only` (disallow symlinks). To choose the approach that fits your usecase, see [file deduplication](https://github.com/9001/copyparty#file-deduplication) in the readme
verification of local file consistency was also added; this happens when someone uploads a dupe, to ensure that no other software has modified the local file since last reindex. This unfortunately makes uploading of duplicate files much slower, and can be disabled with `--safe-dedup 1` if you know that only copyparty will be modifying the filesystem
## new features
* dedup improvements:
* verify consistency of local files before using them as dedup source 6e671c52
* if a local file has been altered by other software since the last reindexing, then this will now be detected
* u2c (commandline uploader): add mode to print hashes of local files 08848be7
* if you've lost a file but you know its `wark` (file identifier), you can now use u2c.exe to scan your whole filesystem for it: `u2c - .`
* #96 use local timezone in log messages b599fbae
## bugfixes
* dedup fixes:
* symlinks could break if moved/renamed inside a volume where deduplication was disabled after some files within had already been deduplicated 4401de04
* when moving/renaming, only consider symlinks between volumes if `xlink` volflag is set b5ad9369
* database consistency verifier (`-e2vp`):
* support filenames with newlines, and warn about missing files b0de84cb
* opengraph/`--og`: fix viewing textfiles e5a836cb
* up2k.js: fix confusing message when uploading many copies of the same file f1130db1
## other changes
* disable upload deduplication by default a2e0f986
* up2k.js: increase handshake timeout to several minutes because of the dedup changes c5988a04
* copyparty.exe: update to python 3.12.6
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0902-0108 `v1.14.4` another
## recent important news
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
## bugfixes
* a network glitch could cause the uploader UI to panic d9e95262
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0830-2311 `v1.14.3` important dedup fix
<img src="https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg" width="250" align="right"/>
* read-only demo server at https://a.ocv.me/pub/demo/
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) [client testbed](https://cd.ocv.me/b/)
there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2023-07-23)
# important bugfix ☢️
this version fixes a file deduplication bug which was introduced in [v1.13.8](https://github.com/9001/copyparty/releases/tag/v1.13.8), released 2024-08-13
its worst-case outcome is **loss of data** in the following scenario:
* someone uploads a file into a folder where that filename is already taken, but the file contents are different, and the server already has a copy of that new file elsewhere under a different name
specific example:
* the server has two existing files, `logo.png` and `logo-v2.png`, in the same volume but not necessarily in the same folder, and those files contain different data
* you have a local copy of `logo-v2.png` on your laptop, but your local filename is `logo.png`
* you upload your local `logo.png` onto the server, into the same folder as the server's `logo.png`
* because the files contain different data, the server accidentally replaces the contents of `logo.png` with your version
if you have been using the database feature (globally with `-e2dsa` or volflag `e2ds`), and you suspect you may have hit this bug, then it is a good idea to make a backup of the up2k databases for all your volumes (the files with names starting with `up2k.db`) before restarting copyparty and before you do anything else, especially if you do not have serverlogs from far back in time -- if you have either the databases and/or the serverlogs, then it is possible to identify replaced files with some manual work
you can check if you hit the bug using one of the following two approaches:
* if your OS has the [gnu find](https://linux.die.net/man/1/find) command, do a search for empty files with `find -type f -size 0`
* using copyparty (any OS), do the following steps:
* make sure that reindex-on-startup is enabled; either globally with `-e2dsa` or volflag `e2ds`
* then install this new copyparty version
* click the search tab `[🔎]` and type the number `0` into the `maximum MiB` textbox
if you find any empty files with a filename that indicates it was autogenerated to avoid a name collision, for example `logo.png-1725040569.239207-kbt0xteO.png`, and the value of the number after `logo.png` is larger than `1723507200` (unixtime for 2024-08-13), then this indicates that `logo.png` may have been replaced by another upload
if you have the serverlogs from when the original upload of `logo.png` was made, then this can be used to identify the original contents of the file that was replaced, and to look for other copies. Please get in touch on the discord for assistance if necessary
----
## new features
* shares: add revival and expiration extension ad2371f8
* share-owners can revive expired shares for `--shr-rt` minutes (default 1 day)
* ...and extend expiration time by adding 1 minute or 1 hour to the timer
* [sfx customizer](https://github.com/9001/copyparty/blob/hovudstraum/scripts/make-sfx.sh) improvements 03b13e8a
* improved translations stripper
* add more examples
## bugfixes
* the dedup bug 3da62ec2
* tftp: support unmapped root 01233991
## other changes
* copyparty.exe: update to pyinstaller 6.10.0
* textviewer wordwrapping c4e2b0f9
* add logo 7037e736 ee359742
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0823-2307 `v1.14.2` bing chilling
@@ -857,7 +1114,7 @@ dnf install https://ocv.me/copyparty/fedora/39/python3-copyparty.fc39.noarch.rpm
## other changes
* improved [systemd example](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd) with hardening and a better example config
* logfiles are flushed for every line written; can be disabled with `--no-logflush` for ~3% more performance best-case
* iphones probably won't broadcast cover-art to car stereos over bluetooth anymore since the thingamajig in iOS that's in charge of that doesn't have cookie-access, and strapping in the auth is too funky so let's stop doing that b7723ac245b8b3e38d6410891ef1aa92d4772114
* iphones probably won't broadcast cover-art to car stereos over bluetooth anymore since the thingamajig in iOS that's in charge of that doesn't have cookie-access, and strapping in the auth is too funky so let's stop doing that b7723ac2
* can be remedied by enabling filekeys and granting unauthenticated people access that way, but that's too much effort for anyone to bother with I'm sure
@@ -1126,12 +1383,12 @@ okay, i swear this is the last version for weeks! probably
* [r0c is much better](https://github.com/9001/r0c) than this joke
## bugfixes
* 163e3fce46122d64bf824762b6733ff2c3551ba5 the `x-forwarded-for` header was ignored if the nearest reverse-proxy is not asking from 127.0.0.1, which broke client IPs in containerized deployments
* 163e3fce the `x-forwarded-for` header was ignored if the nearest reverse-proxy is not asking from 127.0.0.1, which broke client IPs in containerized deployments
* the serverlog will now explain how to trust the reverse-proxy to provide client IPs, but basically,
* `--xff-hdr` specifies which header to read the client's real ip from
* `--xff-src` is an allowlist of IP-addresses to trust that header from
* a62f744a187bc9f821b540e8bb4e0b9a67bd01c8 if copyparty was started while an external HDD was not connected, and that volume's index was stored elsewhere, then the index would get wiped (since all the files are gone)
* 3b8f66c0d5c27a68841814ec06f1758f146a5ff5 javascript could crash while uploading from a very unreliable internet connection
* a62f744a if copyparty was started while an external HDD was not connected, and that volume's index was stored elsewhere, then the index would get wiped (since all the files are gone)
* 3b8f66c0 javascript could crash while uploading from a very unreliable internet connection
## other changes
* copyparty.exe: updated pillow to 10.0.1 which fixes the webp cve
@@ -1177,7 +1434,7 @@ hello! it's been a while, an entire day even...
* default compression levels are gz:3, bz2:2, xz:1; override with `?tar=gz:9`
# bugfixes
* c1efd227b7377144a5760bc6cff64f4e87b626d9 symlink-deduplicated files got indexed with the wrong last-modified timestamp
* c1efd227 symlink-deduplicated files got indexed with the wrong last-modified timestamp
* mostly inconsequential; would cause the dupe's uploader-ip to be forgotten on the next server restart since it would reindex to "fix" the timestamp
* when linking [a search query](https://a.ocv.me/pub/#q=tags%20like%20soundsho*) it loads the results faster
@@ -1192,12 +1449,12 @@ hello! it's been a while, an entire day even...
## new features
* iPhones and iPads are now able to...
* 9986136dfb2364edb35aa9fbb87410641c6d6af3 play entire albums while the screen is off without the music randomly stopping
* 9986136d play entire albums while the screen is off without the music randomly stopping
* apple keeps breaking AudioContext in new and interesting ways; time to give up (no more equalizer)
* 1c0d978979a703edeb792e552b18d3b7695b2d90 perform search queries and execude js code
* 1c0d9789 perform search queries and execude js code
* by translating [smart-quotes](https://stackoverflow.com/questions/48678359/ios-11-safari-html-disable-smart-punctuation) into regular `'` and `"` characters
* python 3.12 support
* technically a bugfix since it was added [a year ago](https://github.com/9001/copyparty/commit/32e22dfe84d5e0b13914b4d0e15c1b8c9725a76d) way before the first py3.12 alpha was released but turns out i botched it, oh well
* technically a bugfix since it was added [a year ago](https://github.com/9001/copyparty/commit/32e22dfe) way before the first py3.12 alpha was released but turns out i botched it, oh well
* filter error messages so they never include the filesystem path where copyparty's python files reside
* print more context in server logs if someone hits an unexpected permission-denied
@@ -1418,8 +1675,8 @@ Thanks for flying copyparty! And especially if you decide to continue doing so :
```bash
(gzip -dc access.log.*.gz; cat access.log) | sed -r 's/" [0-9]+ .*//' | grep -E 'cpr/.*%2[^0]' | grep -vF data:image/svg
```
* 77f1e5144455eb946db7368792ea11c934f0f6da fixes an extremely unlikely race-condition (see the commit for details)
* 8f59afb1593a75b8ce8c91ceee304097a07aea6e fixes another race-condition which is a bit worse:
* 77f1e514 fixes an extremely unlikely race-condition (see the commit for details)
* 8f59afb1 fixes another race-condition which is a bit worse:
* the unpost feature could collide with other database activity, with the worst-case outcome being aborted batch operations, for example a directory move or a batch-rename which stops halfways
----
@@ -1593,7 +1850,7 @@ don't get excited! nothing new and revolutionary, but `xvol` and `xdev` changed
# 2023-0426-2300 `v1.6.15` unexpected boost
## new features
* 30% faster folder listings due to [the very last thing](https://github.com/9001/copyparty/commit/55c74ad164633a0a64dceb51f7f534da0422cbb5) i'd ever expect to be a bottleneck, [thx perf](https://docs.python.org/3.12/howto/perf_profiling.html)
* 30% faster folder listings due to [the very last thing](https://github.com/9001/copyparty/commit/55c74ad1) i'd ever expect to be a bottleneck, [thx perf](https://docs.python.org/3.12/howto/perf_profiling.html)
* option to see the lastmod timestamps of symlinks instead of the target files
* makes the turbo mode of [u2cli, the commandline uploader and folder-sync tool](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) more turbo since copyparty dedupes uploads by symlinking to an existing copy and the symlink is stamped with the deduped file's lastmod
* **webdav:** enabled by default (because rclone will want this), can be disabled with arg `--dav-rt` or volflag `davrt`
@@ -1797,7 +2054,7 @@ don't get excited! nothing new and revolutionary, but `xvol` and `xdev` changed
the commandline up2k upload / filesearch client, now as a standalone windows exe
* based on python 3.7 so it runs on 32bit windows7 or anything newer
* *no https support* (saves space + the python3.7 openssl is getting old)
* built from b39ff92f34e3fca389c78109d20d5454af761f8e so it can do long filepaths and mojibake
* built from b39ff92f so it can do long filepaths and mojibake
----
@@ -2004,7 +2261,7 @@ but nothing is affected (that i know of):
* tar/zip-download of hidden folders
* unpost filtering was buggy for non-ascii characters
* moving a deduplicated file on a volume where deduplication was since disabled
* improved the [linux 6.0.16](https://utcc.utoronto.ca/~cks/space/blog/linux/KernelBindBugIn6016) kernel bug [workaround](https://github.com/9001/copyparty/commit/9065226c3d634a9fc15b14a768116158bc1761ad) because there is similar funk in 5.x
* improved the [linux 6.0.16](https://utcc.utoronto.ca/~cks/space/blog/linux/KernelBindBugIn6016) kernel bug [workaround](https://github.com/9001/copyparty/commit/9065226c) because there is similar funk in 5.x
* add custom text selection colors because chrome is currently broken on fedora
* blockdevs (`/dev/nvme0n1`) couldn't be downloaded as files
* misc fixes for location-based reverse-proxying
@@ -2033,7 +2290,7 @@ hello from warsaw airport (goodbye japan ;_;)
* browser ui didn't allow specifying number of threads for file search
* dont panic if a digit key is pressed while viewing an image
* workaround [linux kernel bug](https://utcc.utoronto.ca/~cks/space/blog/linux/KernelBindBugIn6016) causing log spam on dualstack
* ~~related issue (also mostly harmless) will be fixed next relese 010770684db95bece206943768621f2c7c27bace~~
* ~~related issue (also mostly harmless) will be fixed next relese 01077068~~
* they fixed it in linux 6.1 so these workarounds will be gone too
@@ -2999,7 +3256,7 @@ fixed another dumdum, sorry for the spam
* the ftp server is not compatible with python 3.12 (releasing october 2023)
* will be fixed in a [future version of pyftpdlib](https://github.com/giampaolo/pyftpdlib/issues/560)
the sfx was built from https://github.com/9001/copyparty/commit/39e7a7a2311ab8da43b2a9a18ae39d06202105e3
the sfx was built from https://github.com/9001/copyparty/commit/39e7a7a2
@@ -3780,7 +4037,7 @@ we did it reddit 👉😎👉
* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)
* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)
note: `copyparty-sfx.py` is https://github.com/9001/copyparty/commit/5955940b82adddb7149125a60463aba22f1c8c31 which fixes upload eta
note: `copyparty-sfx.py` is https://github.com/9001/copyparty/commit/5955940b which fixes upload eta
## new features
* provide password using basic-authentication
@@ -5250,7 +5507,7 @@ nothing really important happened since [v0.11.6](https://github.com/9001/copypa
* this release fixes a missing permission check which could allow users to download write-only folders
* this bug was introduced 19 days ago, in `v0.10.17`
* the requirement to be affected is write-only folders mounted within readable folders
* and the worst part is there was a unit-test exactly for this, https://github.com/9001/copyparty/commit/273ca0c8da0d94f9d06ca16bd86c0301d9d06455 way overdue
* and the worst part is there was a unit-test exactly for this, https://github.com/9001/copyparty/commit/273ca0c8 way overdue
* also fixes minor bugs introduced in `v0.11.1`
* this version is the same as `v0.11.5` on pypi
@@ -5434,8 +5691,8 @@ in other news, minor ui tweaks:
* a few lightmode adjustments
* less cpu usage? should be
`copyparty-sfx.py` (latest) made from c5db7c1a0c8f6ab23138ad7ea7642a6260e7da9b (v0.10.15-15) fixes `-j` (multiprocessing/high-performance)
`copyparty-sfx-5a579db.py` (old) made from 5a579dba52e46c202b79c3d80c3b1c996c7b2e4a (v0.10.15-5) reduced the size
`copyparty-sfx.py` (latest) made from c5db7c1a (v0.10.15-15) fixes `-j` (multiprocessing/high-performance)
`copyparty-sfx-5a579db.py` (old) made from 5a579dba (v0.10.15-5) reduced the size
@@ -5748,7 +6005,7 @@ and i just realized i never added runtime tag scanning so copyparty will have to
use `-e2dsa` and `-e2ts` to enable the media tag features globally, or enable/disable them per-volume (see readme)
**NOTE:** older fuse clients (from before 5e3775c1afc9438f9930080a9b8542a063ba1765 / older than v0.8.0) must be upgraded for this copyparty release, however the new client still supports connecting to old servers
**NOTE:** older fuse clients (from before 5e3775c1 / older than v0.8.0) must be upgraded for this copyparty release, however the new client still supports connecting to old servers
other changes include
* support chunked PUT requests from curl
@@ -5968,7 +6225,7 @@ valvrave-stop.jpg
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2020-0818-1822 `v0.5.2` da setter vi punktum
full disclaimer: `copyparty-sfx.py` was built using `sfx.py` from ~~82e568d4c9f25bfdfd1bf5166f0ebedf058723ee~~ f550a8171d298992f4ef569d2fc99a6037a44ea8
full disclaimer: `copyparty-sfx.py` was built using `sfx.py` from ~~82e568d4~~ f550a817

View File

@@ -279,6 +279,7 @@ the rest is mostly optional; if you need a working env for vscode or similar
python3 -m venv .venv
. .venv/bin/activate
pip install jinja2 strip_hints # MANDATORY
pip install argon2-cffi # password hashing
pip install mutagen # audio metadata
pip install pyftpdlib # ftp server
pip install partftpy # tftp server

View File

@@ -16,7 +16,7 @@ open up notepad and save the following as `c:\users\you\documents\party.conf` (f
```yaml
[global]
lo: ~/logs/cpp-%Y-%m%d.xz # log to c:\users\you\logs\
e2dsa, e2ts, no-dedup, z # sets 4 flags; see expl.
e2dsa, e2ts, z # sets 3 flags; see explanation
p: 80, 443 # listen on ports 80 and 443, not 3923
theme: 2 # default theme: protonmail-monokai
lang: nor # default language: viking
@@ -46,11 +46,10 @@ open up notepad and save the following as `c:\users\you\documents\party.conf` (f
### config explained: [global]
the `[global]` section accepts any config parameters [listed here](https://ocv.me/copyparty/helptext.html), also viewable by running copyparty (either the exe or the sfx.py) with `--help`, so this is the same as running copyparty with arguments `--lo c:\users\you\logs\copyparty-%Y-%m%d.xz -e2dsa -e2ts --no-dedup -z -p 80,443 --theme 2 --lang nor`
the `[global]` section accepts any config parameters [listed here](https://ocv.me/copyparty/helptext.html), also viewable by running copyparty (either the exe or the sfx.py) with `--help`, so this is the same as running copyparty with arguments `--lo c:\users\you\logs\copyparty-%Y-%m%d.xz -e2dsa -e2ts -z -p 80,443 --theme 2 --lang nor`
* `lo: ~/logs/cpp-%Y-%m%d.xz` writes compressed logs (the compression will make them delayed)
* `e2dsa` enables the upload deduplicator and file indexer, which enables searching
* `e2dsa` enables the file indexer, which enables searching and upload-undo
* `e2ts` enables music metadata indexing, making albums / titles etc. searchable too
* `no-dedup` writes full dupes to disk instead of symlinking, since lots of windows software doesn't handle symlinks well
* but the improved upload speed from `e2dsa` is not affected
* `z` enables zeroconf, making the server available at `http://HOSTNAME.local/` from any other machine in the LAN
* `p: 80,443` listens on the ports `80` and `443` instead of the default `3923`

View File

@@ -5,7 +5,7 @@
viewBox="0 0 300 207"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
@@ -48,11 +48,12 @@
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="the logo of https://github.com/9001/copyparty">
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>copyparty_logo</dc:title>
<dc:source>github.com/9001/copyparty</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
@@ -101,21 +102,10 @@
r="18" />
<path
style="fill:#737373;stroke-width:1px"
d="m 49,207 6.64,-39.33 c 1.6,-6.3 6.1,-7.7 11.55,-8 58.8,-1.1 106.3,-0.76 165,0 7.4,0.11 10.2,3.25 11.5,8.7 L 251,207 Z"
d="m 48,207 10,-39 c 1.79,-6.2 5.6,-7.8 12,-8 60,-1 100,-1 160,0 6.4,0.2 10,1.8 12,8 l 10,39 z"
id="path1"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="skeu"
style="display:none">
<path
style="fill:#555555;stroke-width:1px"
d="m 48.7,207 6.66,-36.87 c 1.6,-5.9 6.1,-7.2 11.6,-7.5 58.9,-1.06 106.6,-0.7 165.5,0 7.4,0.11 10.2,3.05 11.5,8.12 L 251.3,207 Z"
id="path2"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
@@ -139,39 +129,39 @@
id="layer4"
inkscape:label="stensatt">
<path
d="m 64.54,50.93 q -0.85,0.93 -4.73,2.3 -3.6,1.3 -4.4,1.3 -3.3,0 -5.1,-2.1 -1.75,-2 -1.75,-5.36 0,-4.6 3.76,-7.64 3.3,-2.7 7.3,-2.7 0.4,0 0.93,0.74 0.54,0.7 0.54,1.16 0,2.06 -2.2,2.7 -1.36,0.4 -4.04,1.16 -2.2,1.16 -2.2,4.4 0,3.2 2.9,3.2 0.85,0 0.85,0 0.54,0 1.44,-0.16 1.1,-0.23 2.9,-0.74 1.8,-0.54 2.13,-0.54 0.4,0 1.75,0.6 z"
d="m 63.5,50.9 q -0.85,0.93 -4.73,2.3 -3.6,1.3 -4.4,1.3 -3.3,0 -5.1,-2.1 -1.75,-2 -1.75,-5.36 0,-4.6 3.76,-7.64 3.3,-2.7 7.3,-2.7 0.4,0 0.93,0.74 0.54,0.7 0.54,1.16 0,2.06 -2.2,2.7 -1.36,0.4 -4.04,1.16 -2.2,1.16 -2.2,4.4 0,3.2 2.9,3.2 0.85,0 0.85,0 0.54,0 1.44,-0.16 1.1,-0.23 2.9,-0.74 1.8,-0.54 2.13,-0.54 0.4,0 1.75,0.6 z"
style="fill:#333333"
id="path11" />
<path
d="m 89.2,45.03 q 0,4.2 -3.7,6.95 -3.2,2.3 -6.87,2.3 -3.4,0 -6,-2.6 -2.5,-2.6 -2.5,-6 0,-3.6 3.14,-6.64 3.2,-3 6.8,-3 3.5,0 6.3,2.76 2.83,2.76 2.83,6.25 z m -3.4,0.16 q 0,-2.25 -1.75,-3.7 -1.7,-1.5 -4,-1.5 -0.1,0 -1.6,1.6 -1.44,1.55 -2.44,1.55 -0.6,0 -0.8,-0.3 -1.16,2.3 -1.16,3 0,2.25 2.13,3.4 1.6,0.9 3.6,0.9 2,0 3.76,-1.1 2.25,-1.4 2.25,-3.84 z"
d="m 87.6,45 q 0,4.2 -3.7,6.95 -3.2,2.3 -6.87,2.3 -3.4,0 -6,-2.6 -2.5,-2.6 -2.5,-6 0,-3.6 3.14,-6.64 3.2,-3 6.8,-3 3.5,0 6.3,2.76 2.83,2.76 2.83,6.25 z m -3.4,0.16 q 0,-2.25 -1.75,-3.7 -1.7,-1.5 -4,-1.5 -0.1,0 -1.6,1.6 -1.44,1.55 -2.44,1.55 -0.6,0 -0.8,-0.3 -1.16,2.3 -1.16,3 0,2.25 2.13,3.4 1.6,0.9 3.6,0.9 2,0 3.76,-1.1 2.25,-1.4 2.25,-3.84 z"
style="fill:#333333"
id="path12" />
<path
d="m 114.3,47.94 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.66,1.5 -0.74,0 -2.7,-0.4 -1.9,-0.4 -2.64,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.74,2.83 -1.44,0 -3,-0.85 Q 95.6,53.34 95.6,50.9 q 0,-3.65 1.75,-8.1 2.37,-6.05 6.4,-6.05 3.7,0 7.26,4.1 3.3,3.84 3.3,7.14 z m -3.76,0.2 q -0.66,-2.2 -2.64,-4.4 -2.25,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.85,1.8 -0.85,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.05,0 3.14,0.3 2.13,0.3 3.2,0.3 0.35,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
d="m 112.8,46.8 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2.1,0 -2.1,2.64 0,0.85 0.23,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.77,2.83 -1.44,0 -3,-0.85 -1.46,-9.5 -1.46,-12 0,-3.65 1.75,-8.1 2.37,-6.05 6.45,-6.05 3.7,0 7.3,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.33,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.38,1.24 0.43,0.8 0.85,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path13" />
<path
d="m 133.6,40.2 q -2.06,4.1 -3.2,7 -0.1,0.3 -1.55,4.5 -0.47,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.37,2.64 -1.4,-0.2 -1.55,-1.6 -0.04,-0.2 -0.04,-0.5 0,-0.16 0.3,-1.5 1.05,-5.04 1.05,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.44,-1.36 2.1,-1.36 0.4,0 1.05,0.6 0.6,0.6 0.7,1.1 0.85,6.2 4.9,11.1 1,-1.8 1.86,-4.04 0.5,-1.4 1.6,-4.15 1.86,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.85,0.3 1.24,2.8 z"
d="m 133,40 q -2.1,4.1 -3.2,7 -0.1,0.3 -1.6,4.5 -0.4,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.3,2.64 -1.4,-0.2 -1.6,-1.6 0,-0.2 0,-0.5 0,-0.16 0.3,-1.5 1,-5.04 1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.5,-1.36 2.1,-1.36 0.4,0 1.1,0.6 0.6,0.6 0.7,1.1 0.8,6.2 4.9,11.1 1,-1.8 1.8,-4.04 0.5,-1.4 1.6,-4.15 1.9,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.9,0.3 1.3,2.8 z"
style="fill:#333333"
id="path14" />
<path
d="m 156.97,47.94 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.66,1.5 -0.74,0 -2.7,-0.4 -1.9,-0.4 -2.64,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.74,2.83 -1.44,0 -3,-0.85 -1.44,-9.5 -1.44,-11.95 0,-3.65 1.75,-8.1 2.37,-6.05 6.4,-6.05 3.7,0 7.26,4.1 3.3,3.84 3.3,7.14 z m -3.76,0.2 q -0.66,-2.2 -2.64,-4.4 -2.25,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.85,1.8 -0.85,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.05,0 3.14,0.3 2.13,0.3 3.2,0.3 0.35,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
d="m 157.5,48 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.7,2.83 -1.5,0 -3,-0.85 -1.5,-9.5 -1.5,-11.95 0,-3.65 1.8,-8.1 2.3,-6.05 6.4,-6.05 3.7,0 7.2,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path15" />
<path
d="m 182.4,53.3 q 0,0.9 -0.6,1.5 -0.54,0.6 -1.4,0.6 -1.55,0 -2.95,-0.9 -1.4,-0.93 -2.13,-2.3 -0.74,-0.1 -1.5,0.85 -0.9,1.16 -1.05,1.24 -1.24,0.54 -3.9,0.54 -2.2,0 -3.9,-2.44 -1.5,-2.13 -1.5,-4 0,-3.4 3.34,-6.4 3.2,-2.9 6.7,-2.9 0.9,0 1.67,0.6 0.8,0.6 0.8,1.44 0,0.54 -0.35,1.1 2.37,0.9 2.37,2.83 0,0.35 -0.1,1.05 -0.1,0.7 -0.1,1.05 0,0.4 0.1,0.6 0.5,1.3 2.56,3.4 1.9,1.9 1.9,2.2 z m -8.1,-10.1 q -0.4,0 -1.1,-0.1 -0.74,-0.16 -1.1,-0.16 -1.3,0 -3.2,1.94 -1.9,1.94 -1.9,3.3 0,0.8 0.7,1.8 0.9,1.3 2.2,1.3 2.64,0 3.53,-2.9 0.5,-2.6 1,-5.16 z"
d="m 182,53.3 q 0,0.9 -0.6,1.5 -0.6,0.6 -1.4,0.6 -1.6,0 -3,-0.9 -1.4,-0.93 -2.1,-2.3 -0.7,-0.1 -1.5,0.85 -0.9,1.16 -1.1,1.24 -1.2,0.54 -3.9,0.54 -2.2,0 -3.9,-2.44 -1.5,-2.13 -1.5,-4 0,-3.4 3.4,-6.4 3.2,-2.9 6.7,-2.9 0.9,0 1.7,0.6 0.8,0.6 0.8,1.44 0,0.54 -0.4,1.1 2.4,0.9 2.4,2.83 0,0.35 -0.1,1.05 -0.1,0.7 -0.1,1.05 0,0.4 0.1,0.6 0.5,1.3 2.5,3.4 1.9,1.9 1.9,2.2 z m -8.1,-10.1 q -0.4,0 -1.1,-0.1 -0.8,-0.16 -1.1,-0.16 -1.3,0 -3.2,1.94 -1.9,1.94 -1.9,3.3 0,0.8 0.7,1.8 0.9,1.3 2.2,1.3 2.6,0 3.5,-2.9 0.5,-2.6 1,-5.16 z"
style="fill:#333333"
id="path16" />
<path
d="m 204.8,41.35 q -0.4,0.4 -1.5,0.4 -0.85,0 -2.5,-0.3 -1.67,-0.3 -2.5,-0.3 -4.66,0 -5.43,6.9 -0.35,3.1 -0.4,3.3 -0.4,1 -1.7,2.3 h -1.16 q -0.66,-1.2 -1.3,-4.1 -0.6,-2.76 -0.6,-4.27 0,-1.16 0.1,-1.5 0.2,-0.54 1,-0.54 0.3,0 0.66,0.3 0.4,0.3 0.4,0.3 1.9,-3.53 3.07,-4.6 1.8,-1.7 5.04,-1.7 1.4,0 3.6,0.9 2.83,1.16 3.3,2.8 z"
d="m 203.8,42.4 q -0.4,0.4 -1.5,0.4 -0.9,0 -2.5,-0.3 -1.7,-0.3 -2.5,-0.3 -4.7,0 -5.5,6.9 -0.3,3.1 -0.4,3.3 -0.4,1 -1.7,2.3 h -1.1 q -0.7,-1.2 -1.3,-4.1 -0.6,-2.76 -0.6,-4.27 0,-1.16 0.1,-1.5 0.2,-0.54 1,-0.54 0.3,0 0.6,0.3 0.4,0.3 0.4,0.3 1.9,-3.53 3.1,-4.6 1.8,-1.7 5.1,-1.7 1.4,0 3.6,0.9 2.8,1.16 3.3,2.8 z"
style="fill:#333333"
id="path17" />
<path
d="m 228.46,37.16 q 0.3,0.8 0.3,1.44 0,1.86 -2.4,1.86 -1,0 -3.45,-0.5 -2.5,-0.54 -3.45,-0.54 -1.24,0 -1.44,0.1 -0.47,0.2 -0.47,1.2 0,2.2 0.6,6.9 0.74,5.86 1.6,6.13 -0.35,0.35 -0.35,1.1 -1.24,0.7 -2.64,0.7 -1.4,0 -1.94,-3.9 -0.2,-1.36 -0.5,-7.76 -0.23,-4.6 -0.8,-5.5 -0.3,-0.47 -4.35,-0.35 -1,0.04 -1.55,0.1 -0.54,0 -0.35,0 -0.8,0 -1.2,-0.7 -0.54,-1.3 -0.54,-1.4 0,-1.44 4.15,-2 1.6,-0.16 4.73,-0.5 0,-0.85 -0.1,-2.56 -0.04,-1.75 -0.04,-2.6 0,-4.35 2.1,-4.35 0.54,0 1.1,0.6 0.6,0.6 0.6,1.1 v 7.9 q 1.1,1.2 5,1.7 3.9,0.5 5.3,1.86 z"
d="m 229.5,37.16 q 0.3,0.8 0.3,1.44 0,1.86 -2.4,1.86 -1,0 -3.5,-0.5 -2.5,-0.54 -3.4,-0.54 -1.3,0 -1.5,0.1 -0.4,0.2 -0.4,1.2 0,2.2 0.6,6.9 0.7,5.86 1.6,6.13 -0.4,0.35 -0.4,1.1 -1.2,0.7 -2.6,0.7 -1.4,0 -2,-3.9 -0.2,-1.36 -0.5,-7.76 -0.2,-4.6 -0.8,-5.5 -0.3,-0.47 -4.3,-0.35 -1,0 -1.6,0.1 -0.5,0 -0.3,0 -0.8,0 -1.2,-0.7 -0.5,-1.3 -0.5,-1.4 0,-1.44 4.1,-2 1.6,-0.16 4.7,-0.5 0,-0.85 -0.1,-2.56 0,-1.75 0,-2.6 0,-4.35 2.1,-4.35 0.5,0 1.1,0.6 0.6,0.6 0.6,1.1 v 7.9 q 1.1,1.2 5,1.7 3.9,0.5 5.3,1.86 z"
style="fill:#333333"
id="path18" />
<path
d="m 250.2,40.2 q -2.06,4.1 -3.2,7 -0.1,0.3 -1.55,4.5 -0.47,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.37,2.64 -1.4,-0.2 -1.55,-1.6 -0.04,-0.2 -0.04,-0.5 0,-0.16 0.3,-1.5 1.05,-5.04 1.05,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.44,-1.36 2.1,-1.36 0.4,0 1.05,0.6 0.6,0.6 0.7,1.1 0.85,6.2 4.9,11.1 1,-1.8 1.86,-4.04 0.5,-1.4 1.6,-4.15 1.86,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.85,0.3 1.24,2.8 z"
d="m 251.2,40.2 q -2,4.1 -3.2,7 -0.1,0.3 -1.5,4.5 -0.5,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.4,2.64 -1.4,-0.2 -1.5,-1.6 -0.1,-0.2 -0.1,-0.5 0,-0.16 0.3,-1.5 1.1,-5.04 1.1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.4,-1.36 2.1,-1.36 0.4,0 1,0.6 0.6,0.6 0.7,1.1 0.9,6.2 4.9,11.1 1,-1.8 1.9,-4.04 0.5,-1.4 1.6,-4.15 1.8,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.8,0.3 1.2,2.8 z"
style="fill:#333333"
id="path19" />
</g>

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -255,6 +255,11 @@ cat copyparty/httpcli.py | awk '/^[^a-zA-Z0-9]+def / {printf "%s\n%s\n\n", f, pl
# create a folder with symlinks to big files
for d in /usr /var; do find $d -type f -size +30M 2>/dev/null; done | while IFS= read -r x; do ln -s "$x" big/; done
# up2k worst-case testfiles: create 64 GiB (256 x 256 MiB) of sparse files; each file takes 1 MiB disk space; each 1 MiB chunk is globally unique
for f in {0..255}; do echo $f; truncate -s 256M $f; b1=$(printf '%02x' $f); for o in {0..255}; do b2=$(printf '%02x' $o); printf "\x$b1\x$b2" | dd of=$f bs=2 seek=$((o*1024*1024)) conv=notrunc 2>/dev/null; done; done
# create 6.06G file with 16 bytes of unique data at start+end of each 32M chunk
sz=6509559808; truncate -s $sz f; csz=33554432; sz=$((sz/16)); step=$((csz/16)); ofs=0; while [ $ofs -lt $sz ]; do dd if=/dev/urandom of=f bs=16 count=2 seek=$ofs conv=notrunc iflag=fullblock; [ $ofs = 0 ] && ofs=$((ofs+step-1)) || ofs=$((ofs+step)); done
# py2 on osx
brew install python@2
pip install virtualenv

View File

@@ -63,9 +63,28 @@ add your own translations by using the english or norwegian one from `browser.js
the easy way is to open up and modify `browser.js` in your own installation; depending on how you installed copyparty it might be named `browser.js.gz` instead, in which case just decompress it, restart copyparty, and start editing it anyways
you will be delighted to see inline html in the translation strings; to help prevent syntax errors, there is [a very jank linux script](https://github.com/9001/copyparty/blob/hovudstraum/scripts/tlcheck.sh) which is slightly better than nothing -- just beware the false-positives, so even if it complains it's not necessarily wrong/bad
if you're running `copyparty-sfx.py` then you'll find it at `/tmp/pe-copyparty.1000/copyparty/web` (on linux) or `%TEMP%\pe-copyparty\copyparty\web` (on windows)
* make sure to keep backups of your work religiously! since that location is volatile af
if editing `browser.js` is inconvenient in your setup then you can instead do this:
* add your translation to a separate javascript file (`tl.js`) and make it load before `browser.js` with the help of `--html-head='<script src="/tl.js"></script>'`
* as the page loads, `browser.js` will look for a function named `langmod` so define that function and make it insert your translation into the `Ls` and `LANGS` variables so it'll take effect
## translations (docker-friendly)
if editing `browser.js` is inconvenient in your setup, for example if you're running in docker, then you can instead do this:
* if you have python, go to the `scripts` folder and run `./tl.py fra Français` to generate a `tl.js` which is perfect for translating to French, using the three-letter language code `fra`
* if you do not have python, you can also just grab `tl.js` from the scripts folder, but I'll probably forget to keep that up to date... and then you'll have to find/replace all `"eng"` and `Ls.eng` to your three-letter language code
* put your `tl.js` inside a folder that is being shared by your copyparty, preferably the webroot
* run copyparty with the argument `--html-head='<script src="/tl.js"></script>'`
* if you placed `tl.js` in the webroot then you're all good, but if you put it somewhere else then change `/tl.js` accordingly
* if you are running copyparty with config files, you can do this:
```yaml
[global]
html-head: <script src="/tl.js"></script>
```
you can now edit `tl.js` and press CTRL-SHIFT-R in the browser to see your changes take effect as you go
if you want to contribute your translation back to the project (please do!) then you'll want to...
* grab all of the text inside your `var tl_cpanel = {` and add it to the translations inside `copyparty/web/splash.js` in the repo
* and the text inside your `var tl_browser = {` and add that to the translations inside `copyparty/web/browser.js` in the repo

View File

@@ -3,7 +3,7 @@ WORKDIR /z
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
ver_hashwasm=4.10.0 \
ver_marked=4.3.0 \
ver_dompf=3.1.6 \
ver_dompf=3.1.7 \
ver_mde=2.18.0 \
ver_codemirror=5.65.16 \
ver_fontawesome=5.13.0 \
@@ -37,6 +37,7 @@ RUN mkdir -p /z/dist/no-pk \
&& wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \
&& wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \
&& wget https://github.com/PrismJS/prism/archive/refs/tags/v$ver_prism.tar.gz -O prism.tgz \
&& wget https://files.pythonhosted.org/packages/04/0b/4506cb2e831cea4b0214d3625430e921faaa05a7fb520458c75a2dbd2152/fusepy-3.0.1.tar.gz -O fusepy.tgz \
&& (mkdir hash-wasm \
&& cd hash-wasm \
&& unzip ../hash-wasm.zip) \
@@ -56,6 +57,7 @@ RUN mkdir -p /z/dist/no-pk \
&& npm i gulp-cli -g ) \
&& tar -xf dompurify.tgz \
&& tar -xf prism.tgz \
&& tar -xf fusepy.tgz \
&& unzip fontawesome.zip \
&& tar -xf zopfli.tgz
@@ -158,6 +160,18 @@ RUN cd /z/dist \
&& rmdir no-pk
# build fusepy
COPY uncomment.py /z
RUN mv /z/fusepy-3.0.1/fuse.py /z/dist/f1 \
&& cd /z/dist \
&& python3 /z/uncomment.py f1 \
&& sed -ri '/self.__critical_exception = e/d' f1 \
&& awk '/^log =/{s=0} !s; /^from traceback im/{s=1;print"from functools import partial";print"basestring = str"}' <f1 >f2 \
&& awk '/LoggingMixIn:/{exit} --s<0;/self.use_ns = getattr/{s=7}' <f2 >f1 \
&& awk "/if _machine =/{s=0} /'(mips|ppc|ppc64)'/{s=1} !s" <f1 >f2 \
&& rm f1 && mv f2 fuse.py
# git diff -U2 --no-index marked-1.1.0-orig/ marked-1.1.0-edit/ -U2 | sed -r '/^index /d;s`^(diff --git a/)[^/]+/(.* b/)[^/]+/`\1\2`; s`^(---|\+\+\+) ([ab]/)[^/]+/`\1 \2`' > ../dev/copyparty/scripts/deps-docker/marked-ln.patch
# d=/home/ed/dev/copyparty/scripts/deps-docker/; tar -cf ../x . && ssh root@$bip "cd $d && tar -xv >&2 && make >&2 && tar -cC ../../copyparty/web deps" <../x | (cd ../../copyparty/web/; cat > the.tgz; tar -xvf the.tgz; rm the.tgz)
# gzip -dkf ../dev/copyparty/copyparty/web/deps/deps/marked.full.js.gz && diff -NarU2 ../dev/copyparty/copyparty/web/deps/{,deps/}marked.full.js

View File

@@ -4,8 +4,10 @@ vend := $(self)/../../copyparty/web/deps
# prefers podman-docker (optionally rootless) over actual docker/moby
all:
cp -pv ../uncomment.py .
docker build -t build-copyparty-deps .
rm -rf $(vend)
mkdir $(vend)

View File

@@ -8,7 +8,7 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
ENV XDG_CONFIG_HOME=/cfg
RUN apk --no-cache add !pyc \
wget \
tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow \
ffmpeg

View File

@@ -11,7 +11,7 @@ COPY i/bin/mtag/install-deps.sh ./
COPY i/bin/mtag/audio-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U !pyc \
wget \
tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \
ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \

View File

@@ -8,7 +8,7 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
ENV XDG_CONFIG_HOME=/cfg
RUN apk --no-cache add !pyc \
wget \
tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow py3-mutagen
COPY i/dist/copyparty-sfx.py innvikler.sh ./

View File

@@ -8,7 +8,7 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
ENV XDG_CONFIG_HOME=/cfg
RUN apk add -U !pyc \
wget \
tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \
ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \

View File

@@ -100,11 +100,11 @@ filt=
aa="$(printf '%11s' $a-$i)"
# arm takes forever so make it top priority
[ ${a::3} == arm ] && nice= || nice=nice
[ ${a::3} == arm ] && nice= || nice=-n20
# --pull=never does nothing at all btw
(set -x
$nice podman build \
nice $nice podman build \
--squash \
--pull=never \
--from localhost/alpine-$a \

View File

@@ -6,7 +6,7 @@ import subprocess as sp
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
_ = r""""
echo; for a in '' -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -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]
@@ -71,7 +71,7 @@ def cnv(src):
def main():
src = readclip()
src = re.split("0{100,200}", src[::-1], 1)[1][::-1]
src = re.split("0{100,200}", src[::-1], maxsplit=1)[1][::-1]
with open("helptext.html", "wb") as fo:
for ln in cnv(iter(src.split("\n")[:-3])):
fo.write(ln.encode("utf-8") + b"\r\n")

View File

@@ -19,7 +19,7 @@ exit 0
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
for a in '' -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -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

View File

@@ -11,6 +11,15 @@ gtar=$(command -v gtar || command -v gnutar) || true
realpath() { grealpath "$@"; }
}
tmv() {
touch -r "$1" t
mv t "$1"
}
ised() {
sed -r "$1" <"$2" >t
tmv "$2"
}
targs=(--owner=1000 --group=1000)
[ "$OSTYPE" = msys ] &&
targs=()
@@ -35,6 +44,12 @@ cd pyz
cp -pR ../sfx/{copyparty,partftpy} .
cp -pR ../sfx/{ftp,j2}/* .
true && {
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
echo h > copyparty/web/mde.html
ised '/edit2">edit \(fancy/d' copyparty/web/md.html
}
ts=$(date -u +%s)
hts=$(date -u +%Y-%m%d-%H%M%S)
ver="$(cat ../sfx/ver)"
@@ -42,12 +57,6 @@ ver="$(cat ../sfx/ver)"
mkdir -p ../dist
pyz_out=../dist/copyparty.pyz
echo creating z.tar
( cd copyparty
tar -cf z.tar "${targs[@]}" --numeric-owner web res
rm -rf web res
)
echo creating loader
sed -r 's/^(VER = ).*/\1"'"$ver"'"/; s/^(STAMP = ).*/\1'$(date +%s)/ \
<../scripts/ziploader.py \

View File

@@ -27,14 +27,14 @@ help() { exec cat <<'EOF'
#
# `no-ftp` saves ~30k by removing the ftp server, disabling --ftp
#
# `no-pf` saves ~12k by removing the option to download partyfuse
#
# `no-tfp` saves ~10k by removing the tftp server, disabling --tftp
#
# `no-zm` saves ~7k by removing the zeroconf mDNS server
#
# `no-smb` saves ~3.5k by removing the smb / cifs server
#
# `no-pf` saves ~2.8k by removing the option to download partyfuse
#
# _____________________________________________________________________
# web features:
#
@@ -109,7 +109,7 @@ pybin=$(command -v python3 || command -v python) || {
langs=
use_gz=
zopf=2560
zopf=2000
while [ ! -z "$1" ]; do
case $1 in
clean) clean=1 ; ;;
@@ -146,6 +146,13 @@ ised() {
sed -r "$1" <"$2" >t
tmv "$2"
}
dlf() {
[ -s "$f" ] && return 0
wget -O "$f" "$1" && return 0
curl -L "$1" >"$f" && return 0
rm -f "$f"
exit 1
}
stamp=$(
for d in copyparty scripts; do
@@ -178,8 +185,7 @@ necho() {
necho collecting ipaddress
f="../build/ipaddress-1.0.23.tar.gz"
[ -e "$f" ] ||
(url=https://files.pythonhosted.org/packages/b9/9a/3e9da40ea28b8210dd6504d3fe9fe7e013b62bf45902b458d1cdc3c34ed9/ipaddress-1.0.23.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
dlf https://files.pythonhosted.org/packages/b9/9a/3e9da40ea28b8210dd6504d3fe9fe7e013b62bf45902b458d1cdc3c34ed9/ipaddress-1.0.23.tar.gz
tar -zxf $f
mkdir py37
@@ -189,8 +195,7 @@ necho() {
necho collecting jinja2
f="../build/Jinja2-2.11.3.tar.gz"
[ -e "$f" ] ||
(url=https://files.pythonhosted.org/packages/4f/e7/65300e6b32e69768ded990494809106f87da1d436418d5f1367ed3966fd7/Jinja2-2.11.3.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
dlf https://files.pythonhosted.org/packages/4f/e7/65300e6b32e69768ded990494809106f87da1d436418d5f1367ed3966fd7/Jinja2-2.11.3.tar.gz
tar -zxf $f
mv Jinja2-*/src/jinja2 .
@@ -199,8 +204,7 @@ necho() {
necho collecting markupsafe
f="../build/MarkupSafe-1.1.1.tar.gz"
[ -e "$f" ] ||
(url=https://files.pythonhosted.org/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
dlf https://files.pythonhosted.org/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz
tar -zxf $f
mv MarkupSafe-*/src/markupsafe .
@@ -212,8 +216,7 @@ necho() {
necho collecting pyftpdlib
f="../build/pyftpdlib-1.5.10.tar.gz"
[ -e "$f" ] ||
(url=https://files.pythonhosted.org/packages/cf/31/8d910cf40317dd0db74ba0b8558d0dee23c8b002468c14d3a5dec0e6e9fd/pyftpdlib-1.5.10.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
dlf https://files.pythonhosted.org/packages/cf/31/8d910cf40317dd0db74ba0b8558d0dee23c8b002468c14d3a5dec0e6e9fd/pyftpdlib-1.5.10.tar.gz
tar -zxf $f
mv pyftpdlib-*/pyftpdlib .
@@ -229,8 +232,7 @@ necho() {
necho collecting partftpy
f="../build/partftpy-0.4.0.tar.gz"
[ -e "$f" ] ||
(url=https://files.pythonhosted.org/packages/8c/96/642bb3ddcb07a2c6764eb29aa562d1cf56877ad6c330c3c8921a5f05606d/partftpy-0.4.0.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
dlf https://files.pythonhosted.org/packages/8c/96/642bb3ddcb07a2c6764eb29aa562d1cf56877ad6c330c3c8921a5f05606d/partftpy-0.4.0.tar.gz
tar -zxf $f
mv partftpy-*/partftpy .
@@ -240,8 +242,7 @@ necho() {
v=0.4.27
f="../build/python-magic-$v.tar.gz"
[ -e "$f" ] ||
(url=https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
dlf https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz
tar -zxf $f
mkdir magic
@@ -256,8 +257,7 @@ necho() {
necho collecting strip-hints
f=../build/strip-hints-0.1.10.tar.gz
[ -e $f ] ||
(url=https://files.pythonhosted.org/packages/9c/d4/312ddce71ee10f7e0ab762afc027e07a918f1c0e1be5b0069db5b0e7542d/strip-hints-0.1.10.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
dlf https://files.pythonhosted.org/packages/9c/d4/312ddce71ee10f7e0ab762afc027e07a918f1c0e1be5b0069db5b0e7542d/strip-hints-0.1.10.tar.gz
tar -zxf $f
mv strip-hints-0.1.10/src/strip_hints .
@@ -315,7 +315,7 @@ necho() {
[ ! -e copyparty/web/deps/mini-fa.woff ] && [ $dl_wd ] && {
echo "could not find webdeps; downloading..."
url=https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
wget -Ox.py "$url" || curl -L "$url" >x.py
f=x.py; rm -f $f; dlf $url
echo "extracting webdeps..."
wdsrc="$("$pybin" x.py --version 2>&1 | tee /dev/stderr | awk '/sfxdir:/{sub(/.*: /,"");print;exit}')"
@@ -437,7 +437,7 @@ rm -f ftp/pyftpdlib/{__main__,prefork}.py
rm -rf copyparty/mdns.py copyparty/stolen/dnslib
[ $no_pf ] &&
rm -rf copyparty/web/a/partyfuse.py
rm -rf copyparty/web/a/partyfuse.py copyparty/web/deps/fuse.py
[ $no_cm ] && {
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
@@ -492,8 +492,8 @@ iawk '/^def /{s=0}/^def generate_lorem_ipsum/{s=1}!s' j2/jinja2/utils.py
iawk '/^(class|def) /{s=0}/^(class InternationalizationExtension|def _make_new_n?gettext)/{s=1}!s' j2/jinja2/ext.py
iawk '/^[^ ]/{s=0}/^def babel_extract/{s=1}!s' j2/jinja2/ext.py
ised '/InternationalizationExtension/d' j2/jinja2/ext.py
iawk '/^class/{s=0}/^class (Package|Dict|Function|Prefix|Choice|Module)Loader/{s=1}!s' j2/jinja2/loaders.py
sed -ri '/^from .bccache | (Package|Dict|Function|Prefix|Choice|Module)Loader$/d' j2/jinja2/__init__.py
iawk '/^class/{s=0}/^class (Package|Dict|Prefix|Choice|Module)Loader/{s=1}!s' j2/jinja2/loaders.py
sed -ri '/^from .bccache | (Package|Dict|Prefix|Choice|Module)Loader$/d' j2/jinja2/__init__.py
rm -f j2/jinja2/async* j2/jinja2/{bccache,sandbox}.py
cat > j2/jinja2/_identifier.py <<'EOF'
import re
@@ -609,7 +609,7 @@ pc="bzip2 -"; pe=bz2
[ $use_gzz ] && pc="pigz -11 -I$use_gzz" && pe=gz
echo compressing tar
for n in {2..9}; do cp tar t.$n; nice $pc$n t.$n & done; wait
for n in {2..9}; do cp tar t.$n; nice -n20 $pc$n t.$n & done; wait
minf=$(for f in t.*.$pe; do
s1=$(wc -c <$f)
s2=$(tr -d '\r\n\0' <$f | wc -c)

View File

@@ -77,11 +77,14 @@ excl=(
email._header_value_parser
email.header
email.parser
importlib.resources
importlib_resources
inspect
multiprocessing
packaging
pdb
pickle
pkg_resources
PIL.EpsImagePlugin
pyftpdlib.prefork
urllib.request

View File

@@ -11,14 +11,10 @@ ckpypi() {
pyinstaller
pyinstaller-hooks-contrib
pywin32-ctypes
certifi
charset_normalizer
idna
Jinja2
MarkupSafe
mutagen
Pillow
requests
)
for dep in "${deps[@]}"; do
k=

View File

@@ -4,12 +4,6 @@ f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f8
749a473646c6d4c7939989649733d4c7699fd1c359c27046bf5bc9c070d1a4b8b986bbc65f60d7da725baf16dbfdd75a4c2f5bb8335f2cb5685073f5fee5c2d1 pywin32_ctypes-0.2.2-py3-none-any.whl
085d39ef4426aa5f097fbc484595becc16e61ca23fc7da4d2a8bba540a3b82e789e390b176c7151bdc67d01735cce22b1562cdb2e31273225a2d3e275851a4ad setuptools-70.3.0-py3-none-any.whl
360a141928f4a7ec18a994602cbb28bbf8b5cc7c077a06ac76b54b12fa769ed95ca0333a5cf728923a8e0baeb5cc4d5e73e5b3de2666beb05eb477d8ae719093 upx-4.2.4-win32.zip
# u2c (win7)
7a3bd4849f95e1715fe2e99613df70a0fedd944a9bfde71a0fadb837fe62c3431c30da4f0b75c74de6f1a459f1fdf7cb62eaf404fdbe45e2d121e0b1021f1580 certifi-2024.2.2-py3-none-any.whl
9cc8acc5e269e6421bc32bb89261101da29d6ca337d39d60b9106de9ed7904e188716e4a48d78a2c4329026443fcab7acab013d2fe43778e30d6c4e4506a1b91 charset_normalizer-3.3.2-cp37-cp37m-win32.whl
0ec1ae5c928b4a0001a254c8598b746049406e1eed720bfafa94d4474078eff76bf6e032124e2d4df4619052836523af36162443c6d746487b387d2e3476e691 idna-3.6-py3-none-any.whl
cc08d0d87d184401872a2f82266d589253979b4cd02f23b51290fbb2a20082848fc72acbed8aacb74ac4af068d575ef96e66196c5068bc38fb0bcafdc7626869 requests-2.29.0-py3-none-any.whl
fe5fee6cb8a2c68800b32353a0015e5d2e1ad1cb6e0c9e6acf86e48e5cdb5606ad465dc4485ea5fbc8701d8716a8a7f7148c57724ef9da26b0c0a76f6dbbd698 urllib3-1.26.19-py2.py3-none-any.whl
# win7
3253e86471e6f9fa85bfdb7684cd2f964ed6e35c6a4db87f81cca157c049bef43e66dfcae1e037b2fb904567b1e028aaeefe8983ba3255105df787406d2aa71e en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso
ab0db0283f61a5bbe44797d74546786bf41685175764a448d2e3bd629f292f1e7d829757b26be346b5044d78c9c1891736d93237cee4b1b6f5996a902c86d15f en_windows_7_professional_with_sp1_x64_dvd_u_676939.iso
@@ -36,4 +30,4 @@ d1420c8417fad7888766dd26b9706a87c63e8f33dceeb8e26d0056d5127b0b3ed9272e44b4b76113
2be320b4191f208cdd6af183c77ba2cf460ea52164ee45ac3ff17d6dfa57acd9deff016636c2dd42a21f4f6af977d5f72df7dacf599bebcf41757272354d14c1 pillow-10.4.0-cp312-cp312-win_amd64.whl
896ddddbd4b85e86e0600cb65eb4c07fbc7f3802d47e7f660411e20b5500831469b97ed4770f25820f4e75cbfac40308da624fd86d4f62e578149d5c276a9cde pyinstaller-6.10.0-py3-none-win_amd64.whl
873781decaeef07f6a79b0ed8b9f35f3fa534a1ea0d866991e40278a10818fa5b60c70b0d5828971b045364f1099694cd1e5d5d60d480acb93fcfbfbced4a09e pyinstaller_hooks_contrib-2024.8-py3-none-any.whl
0572c6345f6a4f7f3e5c2ff858e3ca7ca54ae4478f3d59d8e18cb0f596e61dcf12aef579db229e83d63b30f15d6684ee6bb3feaea9413e5e636a503933057678 python-3.12.5-amd64.exe
912b710007c7b29f29c0097aff8f825412166eed7777a7cef135b14316e8fff31b5df56d26d835d8ca090468cc0e914730f201a56caa3dd6dbef2f91088942b1 python-3.12.7-amd64.exe

View File

@@ -13,13 +13,6 @@ https://pypi.org/project/MarkupSafe/#files
https://pypi.org/project/mutagen/#files
https://pypi.org/project/Pillow/#files
# u2c (win7) additionals
https://pypi.org/project/certifi/#files
https://pypi.org/project/charset-normalizer/#files # cp37-cp37m-win32.whl
https://pypi.org/project/idna/#files
https://pypi.org/project/requests/#files
https://pypi.org/project/urllib3/#files
# win7 additionals
https://pypi.org/project/future/#files
https://pypi.org/project/importlib-metadata/#files

View File

@@ -41,14 +41,7 @@ fns=(
pillow-10.4.0-cp312-cp312-win_amd64.whl
pyinstaller-6.10.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2024.8-py3-none-any.whl
python-3.12.5-amd64.exe
)
[ $w7 ] && fns+=( # u2c stuff
certifi-2024.2.2-py3-none-any.whl
charset_normalizer-3.3.2-cp37-cp37m-win32.whl
idna-3.6-py3-none-any.whl
requests-2.29.0-py3-none-any.whl
urllib3-1.26.19-py2.py3-none-any.whl
python-3.12.7-amd64.exe
)
[ $w7 ] && fns+=(
future-1.0.0-py3-none-any.whl
@@ -96,12 +89,11 @@ python -m ensurepip &&
{ [ $w10 ] || python -m pip install --user -U pip-*.whl; } &&
python -m pip install --user -U packaging-*.whl &&
{ [ $w7 ] || python -m pip install --user -U {setuptools,mutagen,pillow,jinja2,MarkupSafe}-*.whl; } &&
{ [ $w10 ] || python -m pip install --user -U {requests,urllib3,charset_normalizer,certifi,idna}-*.whl; } &&
{ [ $w10 ] || python -m pip install --user -U future-*.whl importlib_metadata-*.whl typing_extensions-*.whl zipp-*.whl; } &&
python -m pip install --user -U pyinstaller-*.whl pefile-*.whl pywin32_ctypes-*.whl pyinstaller_hooks_contrib-*.whl altgraph-*.whl &&
sed -ri 's/--lzma/--best/' $appd/Python/Python$pyv/site-packages/pyinstaller/building/utils.py &&
curl -fkLO https://192.168.123.1:3923/cpp/scripts/uncomment.py &&
python uncomment.py 1 $(for d in $appd/Python/Python$pyv/site-packages/{requests,urllib3,charset_normalizer,certifi,idna,mutagen,PIL,jinja2,markupsafe}; do find $d -name \*.py; done) &&
python uncomment.py 1 $(for d in $appd/Python/Python$pyv/site-packages/{mutagen,PIL,jinja2,markupsafe}; do find $d -name \*.py; done) &&
cd &&
rm -f build.sh &&
curl -fkLO https://192.168.123.1:3923/cpp/scripts/pyinstaller/build.sh &&

View File

@@ -19,25 +19,12 @@ dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.ico
dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.rc
dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.spec
# $LOCALAPPDATA/programs/python/python37-32/python -m pip install --user -U pyinstaller requests
# $LOCALAPPDATA/programs/python/python37-32/python -m pip install --user -U pyinstaller
grep -E '^from .ssl_ import' $APPDATA/python/python37/site-packages/urllib3/util/proxy.py && {
echo golfing
echo > $APPDATA/python/python37/site-packages/requests/certs.py
sed -ri 's/^(DEFAULT_CA_BUNDLE_PATH = ).*/\1""/' $APPDATA/python/python37/site-packages/requests/utils.py
sed -ri '/^import zipfile$/d' $APPDATA/python/python37/site-packages/requests/utils.py
sed -ri 's/"idna"//' $APPDATA/python/python37/site-packages/requests/packages.py
sed -ri 's/import charset_normalizer.*/pass/' $APPDATA/python/python37/site-packages/requests/compat.py
sed -ri 's/raise.*charset_normalizer.*/pass/' $APPDATA/python/python37/site-packages/requests/__init__.py
sed -ri 's/import charset_normalizer.*//' $APPDATA/python/python37/site-packages/requests/packages.py
sed -ri 's/chardet.__name__/"\\roll\\tide"/' $APPDATA/python/python37/site-packages/requests/packages.py
sed -ri 's/chardet,//' $APPDATA/python/python37/site-packages/requests/models.py
for n in util/__init__.py connection.py; do awk -i inplace '/^from (\.util)?\.ssl_ /{s=1} !s; /^\)/{s=0}' $APPDATA/python/python37/site-packages/urllib3/$n; done
sed -ri 's/^from .ssl_ import .*//' $APPDATA/python/python37/site-packages/urllib3/util/proxy.py
echo golfed
}
sed -ri 's/^(import .*), selectors$/\1\ntry: import selectors\nexcept: pass/' $LOCALAPPDATA/programs/python/python37-32/Lib/socket.py
sed -ri 's/(add_argument."-t[de]",.*help=")[^"]+/\1not applicable; HTTPS is disabled in this exe/; s/for some reason/in this exe for safety reasons/' u2c.py
sed -ri '/^import platform/d;s/^(VT100 = )pla.*/\1False/' u2c.py
read a b _ < <(awk -F\" '/^S_VERSION =/{$0=$2;sub(/\./," ");print}' < u2c.py)
sed -r 's/1,2,3,0/'$a,$b,0,0'/;s/1\.2\.3/'$a.$b.0/ <up2k.rc >up2k.rc2

View File

@@ -14,22 +14,21 @@ a = Analysis(
hooksconfig={},
runtime_hooks=[],
excludes=[
'bz2',
'ftplib',
'getpass',
'lzma',
'pickle',
'platform',
'selectors',
'ssl',
'subprocess',
'tarfile',
'bz2',
'zipfile',
'tempfile',
'tracemalloc',
'typing',
'zipfile',
'zlib',
'urllib3.util.ssl_',
'urllib3.contrib.pyopenssl',
'urllib3.contrib.socks',
'certifi',
'idna',
'chardet',
'charset_normalizer',
'email.contentmanager',
'email.policy',
'encodings.zlib_codec',
@@ -40,6 +39,8 @@ a = Analysis(
'encodings.palmos',
'encodings.punycode',
'encodings.rot_13',
'urllib.response',
'urllib.robotparser',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,

View File

@@ -6,7 +6,6 @@ set -e
ex=(
ftplib lzma pickle ssl tarfile bz2 zipfile tracemalloc zlib
urllib3.util.ssl_ urllib3.contrib.pyopenssl urllib3.contrib.socks certifi idna chardet charset_normalizer
email.contentmanager email.policy
encodings.{zlib_codec,base64_codec,bz2_codec,charmap,hex_codec,palmos,punycode,rot_13}
);

View File

@@ -84,6 +84,7 @@ copyparty/web/deps/__init__.py,
copyparty/web/deps/busy.mp3,
copyparty/web/deps/easymde.css,
copyparty/web/deps/easymde.js,
copyparty/web/deps/fuse.py,
copyparty/web/deps/marked.js,
copyparty/web/deps/mini-fa.css,
copyparty/web/deps/mini-fa.woff,

View File

@@ -61,15 +61,29 @@ def uh2(fp):
# remove expensive imports too
lns = []
on = True
on2 = True
for ln in cs.split("\n"):
if ln.startswith("if True:"):
on = False
continue
if ln.endswith("# !rm.yes>"):
on2 = False
continue
if not on2:
if ln.endswith("# !rm.no>"):
on2 = True
continue
if not on and (not ln.strip() or ln.startswith(" ")):
continue
on = True
if " # !rm" in ln:
continue
lns.append(ln)
cs = "\n".join(lns)

View File

@@ -98,6 +98,7 @@ def tc1(vflags):
args = [
"-q",
"-j0",
"-p4321",
"-e2dsa",
"-e2tsr",

View File

@@ -3,7 +3,7 @@ set -ex
# PYTHONPATH=.:~/dev/partftpy/ taskset -c 0 python3 -m copyparty -v srv::r -v srv/junk:junk:A --tftp 3969
get_src=~/dev/copyparty/srv/ro/palette.flac
get_src=~/dev/copyparty/srv/ro/palette.flac # tftpwa...
get_fp=ro/${get_src##*/} # server url
get_fn=${get_fp##*/} # just filename

657
scripts/tl.js Normal file
View File

@@ -0,0 +1,657 @@
"use strict";
// NOTE: please use tabs for indenting, not spaces :-)
// NOTE: since you are using the tl.js straight from the repo, you'll
// need to find/replace all "eng" with your own three-letter name
// the three-letter name of the language you're translating to
var my_lang = "eng";
// and because we don't know these yet...
var SR='', wah='';
// this function is automatically called when the page is loaded:
function langmod() {
// which page is the javascript currently running on?
// look for some well-known elements to figure it out:
if (QS("h1#cc") && QS("a#k")) {
// we are running in the control-panel
Ls[my_lang] = tl_cpanel[my_lang];
}
else if (ebi("op_cfg")) {
// we are running in the filebrowser
Ls[my_lang] = tl_browser[my_lang];
// inform the settings tab that a new lang is available
LANGS.push(my_lang);
}
}
////////////////////////////////////////////////////////////////////////
// below this point is where the actual translation happens;
// here is the pairs of "text-identifier": "text-to-translate"
////////////////////////////////////////////////////////////////////////
// translation of splash.js (the control-panel);
// you do not need to translate the TLNotes, those are just for you :-)
var tl_cpanel = {
"eng": {
"a1": "refresh",
"b1": "howdy stranger &nbsp; <small>(you're not logged in)</small>",
"c1": "logout",
"d1": "dump stack", // TLNote: "d2" is the tooltip for this button
"d2": "shows the state of all active threads",
"e1": "reload cfg",
"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",
"f1": "you can browse:",
"g1": "you can upload to:",
"cc1": "other stuff:",
"h1": "disable k304", // TLNote: "j1" explains what k304 is
"i1": "enable k304",
"j1": "enabling this 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",
"k1": "reset client settings",
"l1": "login for more:",
"m1": "welcome back,", // TLNote: "welcome back, USERNAME"
"n1": "404 not found &nbsp;┐( ´ -`)┌",
"o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
"p1": "403 forbiddena &nbsp;~┻━┻",
"q1": 'use a password or <a href="' + SR + '/?h">go home</a>',
"r1": "go home",
".s1": "rescan",
"t1": "action", // TLNote: this is the header above the "rescan" buttons
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v1": "connect",
"v2": "use this server as a local HDD",
"w1": "switch to https",
"x1": "change password",
"y1": "edit shares", // TLNote: shows the list of folders that the user has decided to share
"z1": "unlock this share:", // TLNote: the password prompt to see a hidden share
"ta1": "fill in your new password first",
"ta2": "repeat to confirm new password:",
"ta3": "found a typo; please try again",
"aa1": "incoming files:",
},
};
////////////////////////////////////////////////////////////////////////
// translation of browser.js (the filebrowser):
var tl_browser = {
"eng": {
"tt": "English",
"cols": {
"c": "action buttons",
"dur": "duration",
"q": "quality / bitrate",
"Ac": "audio codec",
"Vc": "video codec",
"Fmt": "format / container",
"Ahash": "audio checksum",
"Vhash": "video checksum",
"Res": "resolution",
"T": "filetype",
"aq": "audio quality / bitrate",
"vq": "video quality / bitrate",
"pixfmt": "subsampling / pixel structure",
"resw": "horizontal resolution",
"resh": "vertical resolution",
"chs": "audio channels",
"hz": "sample rate"
},
"hks": [
[
"misc",
["ESC", "close various things"],
"file-manager",
["G", "toggle list / grid view"],
["T", "toggle thumbnails / icons"],
["🡅 A/D", "thumbnail size"],
["ctrl-K", "delete selected"],
["ctrl-X", "cut selected"],
["ctrl-V", "paste into folder"],
["Y", "download selected"],
["F2", "rename selected"],
"file-list-sel",
["space", "toggle file selection"],
["🡑/🡓", "move selection cursor"],
["ctrl 🡑/🡓", "move cursor and viewport"],
["🡅 🡑/🡓", "select prev/next file"],
["ctrl-A", "select all files / folders"],
], [
"navigation",
["B", "toggle breadcrumbs / navpane"],
["I/K", "prev/next folder"],
["M", "parent folder (or unexpand current)"],
["V", "toggle folders / textfiles in navpane"],
["A/D", "navpane size"],
], [
"audio-player",
["J/L", "prev/next song"],
["U/O", "skip 10sec back/fwd"],
["0..9", "jump to 0%..90%"],
["P", "play/pause (also initiates)"],
["Y", "download song"],
], [
"image-viewer",
["J/L, ←/→", "prev/next pic"],
["Home/End", "first/last pic"],
["F", "fullscreen"],
["R", "rotate clockwise"],
["🡅 R", "rotate ccw"],
["Y", "download pic"],
], [
"video-player",
["U/O", "skip 10sec back/fwd"],
["P/K/Space", "play/pause"],
["C", "continue playing next"],
["V", "loop"],
["M", "mute"],
["[ and ]", "set loop interval"],
], [
"textfile-viewer",
["I/K", "prev/next file"],
["M", "close textfile"],
["E", "edit textfile"],
["S", "select file (for cut/rename)"],
]
],
"m_ok": "OK",
"m_ng": "Cancel",
"enable": "Enable",
"danger": "DANGER",
"clipped": "copied to clipboard",
"ht_s1": "second",
"ht_s2": "seconds",
"ht_m1": "minute",
"ht_m2": "minutes",
"ht_h1": "hour",
"ht_h2": "hours",
"ht_d1": "day",
"ht_d2": "days",
"ht_and": " and ",
"goh": "control-panel",
"gop": 'previous sibling">prev',
"gou": 'parent folder">up',
"gon": 'next folder">next',
"logout": "Logout ",
"access": " access",
"ot_close": "close submenu",
"ot_search": "search for files by attributes, path / name, music tags, or any combination of those$N$N&lt;code&gt;foo bar&lt;/code&gt; = must contain both «foo» and «bar»,$N&lt;code&gt;foo -bar&lt;/code&gt; = must contain «foo» but not «bar»,$N&lt;code&gt;^yana .opus$&lt;/code&gt; = start with «yana» and be an «opus» file$N&lt;code&gt;&quot;try unite&quot;&lt;/code&gt; = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N&lt;code&gt;2009-12-31&lt;/code&gt; or &lt;code&gt;2020-09-12 23:30:00&lt;/code&gt;",
"ot_unpost": "unpost: delete your recent uploads, or abort unfinished ones",
"ot_bup": "bup: basic uploader, even supports netscape 4.0",
"ot_mkdir": "mkdir: create a new directory",
"ot_md": "new-md: create a new markdown document",
"ot_msg": "msg: send a message to the server log",
"ot_mp": "media player options",
"ot_cfg": "configuration options",
"ot_u2i": 'up2k: upload files (if you have write-access) or toggle into the search-mode to see if they exist somewhere on the server$N$Nuploads are resumable, multithreaded, and file timestamps are preserved, but it uses more CPU than [🎈]&nbsp; (the basic uploader)<br /><br />during uploads, this icon becomes a progress indicator!',
"ot_u2w": 'up2k: upload files with resume support (close your browser and drop the same files in later)$N$Nmultithreaded, and file timestamps are preserved, but it uses more CPU than [🎈]&nbsp; (the basic uploader)<br /><br />during uploads, this icon becomes a progress indicator!',
"ot_noie": 'Please use Chrome / Firefox / Edge',
"ab_mkdir": "make directory",
"ab_mkdoc": "new markdown doc",
"ab_msg": "send msg to srv log",
"ay_path": "skip to folders",
"ay_files": "skip to files",
"wt_ren": "rename selected items$NHotkey: F2",
"wt_del": "delete selected items$NHotkey: ctrl-K",
"wt_cut": "cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X",
"wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V",
"wt_selall": "select all files$NHotkey: ctrl-A (when file focused)",
"wt_selinv": "invert selection",
"wt_selzip": "download selection as archive",
"wt_seldl": "download selection as separate files$NHotkey: Y",
"wt_npirc": "copy irc-formatted track info",
"wt_nptxt": "copy plaintext track info",
"wt_grid": "toggle grid / list view$NHotkey: G",
"wt_prev": "previous track$NHotkey: J",
"wt_play": "play / pause$NHotkey: P",
"wt_next": "next track$NHotkey: L",
"ul_par": "parallel uploads:",
"ut_rand": "randomize filenames",
"ut_u2ts": "copy the last-modified timestamp$Nfrom your filesystem to the server",
"ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck",
"ut_ask": 'ask for confirmation before upload starts">💭',
"ut_pot": "improve upload speed on slow devices$Nby making the UI less complex",
"ut_srch": "don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)",
"ut_par": "pause uploads by setting it to 0$N$Nincrease if your connection is slow / high latency$N$Nkeep it 1 on LAN or if the server HDD is a bottleneck",
"ul_btn": "drop files / folders<br>here (or click me)",
"ul_btnu": "U P L O A D",
"ul_btns": "S E A R C H",
"ul_hash": "hash",
"ul_send": "send",
"ul_done": "done",
"ul_idle1": "no uploads are queued yet",
"ut_etah": "average &lt;em&gt;hashing&lt;/em&gt; speed, and estimated time until finish",
"ut_etau": "average &lt;em&gt;upload&lt;/em&gt; speed and estimated time until finish",
"ut_etat": "average &lt;em&gt;total&lt;/em&gt; speed and estimated time until finish",
"uct_ok": "completed successfully",
"uct_ng": "no-good: failed / rejected / not-found",
"uct_done": "ok and ng combined",
"uct_bz": "hashing or uploading",
"uct_q": "idle, pending",
"utl_name": "filename",
"utl_ulist": "list",
"utl_ucopy": "copy",
"utl_links": "links",
"utl_stat": "status",
"utl_prog": "progress",
// keep short:
"utl_404": "404",
"utl_err": "ERROR",
"utl_oserr": "OS-error",
"utl_found": "found",
"utl_defer": "defer",
"utl_yolo": "YOLO",
"utl_done": "done",
"ul_flagblk": "the files were added to the queue</b><br>however there is a busy up2k in another browser tab,<br>so waiting for that to finish first",
"ul_btnlk": "the server configuration has locked this switch into this state",
"udt_up": "Upload",
"udt_srch": "Search",
"udt_drop": "drop it here",
"u_nav_m": '<h6>aight, what do you have?</h6><code>Enter</code> = Files (one or more)\n<code>ESC</code> = One folder (including subfolders)',
"u_nav_b": '<a href="#" id="modal-ok">Files</a><a href="#" id="modal-ng">One folder</a>',
"cl_opts": "switches",
"cl_themes": "theme",
"cl_langs": "language",
"cl_ziptype": "folder download",
"cl_uopts": "up2k switches",
"cl_favico": "favicon",
"cl_bigdir": "big dirs",
"cl_keytype": "key notation",
"cl_hiddenc": "hidden columns",
"cl_hidec": "hide",
"cl_reset": "reset",
"cl_hpick": "tap on column headers to hide in the table below",
"cl_hcancel": "column hiding aborted",
"ct_grid": '田 the grid',
"ct_ttips": '◔ ◡ ◔"> tooltips',
"ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs',
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
"ct_dots": 'show hidden files (if server permits)">dotfiles',
"ct_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_readme": 'show README.md in folder listings">📜 readme',
"ct_idxh": 'show index.html instead of folder listing">htm',
"ct_sbars": 'show scrollbars">⟊',
"cut_umod": "if a file already exists on the server, update the server's last-modified timestamp to match your local file (requires write+delete permissions)\">re📅",
"cut_turbo": "the yolo button, you probably DO NOT want to enable this:$N$Nuse this if you were uploading a huge amount of files and had to restart for some reason, and want to continue the upload ASAP$N$Nthis replaces the hash-check with a simple <em>&quot;does this have the same filesize on the server?&quot;</em> so if the file contents are different it will NOT be uploaded$N$Nyou should turn this off when the upload is done, and then &quot;upload&quot; the same files again to let the client verify them\">turbo",
"cut_datechk": "has no effect unless the turbo button is enabled$N$Nreduces the yolo factor by a tiny amount; checks whether the file timestamps on the server matches yours$N$Nshould <em>theoretically</em> catch most unfinished / corrupted uploads, but is not a substitute for doing a verification pass with turbo disabled afterwards\">date-chk",
"cut_u2sz": "size (in MiB) of each upload chunk; big values fly better across the atlantic. Try low values on very unreliable connections",
"cut_flag": "ensure only one tab is uploading at a time $N -- other tabs must have this enabled too $N -- only affects tabs on the same domain",
"cut_az": "upload files in alphabetical order, rather than smallest-file-first$N$Nalphabetical order can make it easier to eyeball if something went wrong on the server, but it makes uploading slightly slower on fiber / LAN",
"cut_nag": "OS notification when upload completes$N(only if the browser or tab is not active)",
"cut_sfx": "audible alert when upload completes$N(only if the browser or tab is not active)",
"cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$N30% faster https, 4.5x faster http,$Nand 5.3x faster on android phones\">mt",
"cft_text": "favicon text (blank and refresh to disable)",
"cft_fg": "foreground color",
"cft_bg": "background color",
"cdt_lim": "max number of files to show in a folder",
"cdt_ask": "when scrolling to the bottom,$Ninstead of loading more files,$Nask what to do",
"tt_entree": "show navpane (directory tree sidebar)$NHotkey: B",
"tt_detree": "show breadcrumbs$NHotkey: B",
"tt_visdir": "scroll to selected folder",
"tt_ftree": "toggle folder-tree / textfiles$NHotkey: V",
"tt_pdock": "show parent folders in a docked pane at the top",
"tt_dynt": "autogrow as tree expands",
"tt_wrap": "word wrap",
"tt_hover": "reveal overflowing lines on hover$N( breaks scrolling unless mouse $N&nbsp; cursor is in the left gutter )",
"ml_pmode": "at end of folder...",
"ml_btns": "cmds",
"ml_tcode": "transcode",
"ml_tint": "tint",
"ml_eq": "audio equalizer",
"ml_drc": "dynamic range compressor",
"mt_shuf": "shuffle the songs in each folder\">🔀",
"mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶",
"mt_preload": "start loading the next song near the end for gapless playback\">preload",
"mt_prescan": "go to the next folder before the last song$Nends, keeping the webbrowser happy$Nso it doesn't stop the playback\">nav",
"mt_fullpre": "try to preload the entire song;$N✅ enable on <b>unreliable</b> connections,$N❌ <b>disable</b> on slow connections probably\">full",
"mt_fau": "on phones, prevent music from stopping if the next song doesn't preload fast enough (can make tags display glitchy)\">☕️",
"mt_waves": "waveform seekbar:$Nshow audio amplitude in the scrubber\">~s",
"mt_npclip": "show buttons for clipboarding the currently playing song\">/np",
"mt_octl": "os integration (media hotkeys / osd)\">os-ctl",
"mt_oseek": "allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\">seek",
"mt_oscv": "show album cover in osd\">art",
"mt_follow": "keep the playing track scrolled into view\">🎯",
"mt_compact": "compact controls\">⟎",
"mt_uncache": "clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache",
"mt_mloop": "loop the open folder\">🔁 loop",
"mt_mnext": "load the next folder and continue\">📂 next",
"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_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting",
"mt_eq": "enables the equalizer and gain control;$N$Nboost &lt;code&gt;0&lt;/code&gt; = standard 100% volume (unmodified)$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = standard stereo (unmodified)$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% left-right crossfeed$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = vocal removal :^)$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero (except width = 1) if you care about that",
"mt_drc": "enables the dynamic range compressor (volume flattener / brickwaller); will also enable EQ to balance the spaghetti, so set all EQ fields except for 'width' to 0 if you don't want it$N$Nlowers the volume of audio above THRESHOLD dB; for every RATIO dB past THRESHOLD there is 1 dB of output, so default values of tresh -24 and ratio 12 means it should never get louder than -22 dB and it is safe to increase the equalizer boost to 0.8, or even 1.8 with ATK 0 and a huge RLS like 90 (only works in firefox; RLS is max 1 in other browsers)$N$N(see wikipedia, they explain it much better)",
"mb_play": "play",
"mm_hashplay": "play this audio file?",
"mp_breq": "need firefox 82+ or chrome 73+ or iOS 15+",
"mm_bload": "now loading...",
"mm_bconv": "converting to {0}, please wait...",
"mm_opusen": "your browser cannot play aac / m4a files;\ntranscoding to opus is now enabled",
"mm_playerr": "playback failed: ",
"mm_eabrt": "The playback attempt was cancelled",
"mm_enet": "Your internet connection is wonky",
"mm_edec": "This file is supposedly corrupted??",
"mm_esupp": "Your browser does not understand this audio format",
"mm_eunk": "Unknown Errol",
"mm_e404": "Could not play audio; error 404: File not found.",
"mm_e403": "Could not play audio; error 403: Access denied.\n\nTry pressing F5 to reload, maybe you got logged out",
"mm_e5xx": "Could not play audio; server error ",
"mm_nof": "not finding any more audio files nearby",
"mm_prescan": "Looking for music to play next...",
"mm_scank": "Found the next song:",
"mm_uncache": "cache cleared; all songs will redownload on next playback",
"mm_hnf": "that song no longer exists",
"im_hnf": "that image no longer exists",
"f_empty": 'this folder is empty',
"f_chide": 'this will hide the column «{0}»\n\nyou can unhide columns in the settings tab',
"f_bigtxt": "this file is {0} MiB large -- really view as text?",
"fbd_more": '<div id="blazy">showing <code>{0}</code> of <code>{1}</code> files; <a href="#" id="bd_more">show {2}</a> or <a href="#" id="bd_all">show all</a></div>',
"fbd_all": '<div id="blazy">showing <code>{0}</code> of <code>{1}</code> files; <a href="#" id="bd_all">show all</a></div>',
"f_dls": 'the file links in the current folder have\nbeen changed into download links',
"f_partial": "To safely download a file which is currently being uploaded, please click the file which has the same filename, but without the <code>.PARTIAL</code> file extension. Please press CANCEL or Escape to do this.\n\nPressing OK / Enter will ignore this warning and continue downloading the <code>.PARTIAL</code> scratchfile instead, which will almost definitely give you corrupted data.",
"ft_paste": "paste {0} items$NHotkey: ctrl-V",
"fr_eperm": 'cannot rename:\nyou do not have “move” permission in this folder',
"fd_eperm": 'cannot delete:\nyou do not have “delete” permission in this folder',
"fc_eperm": 'cannot cut:\nyou do not have “move” permission in this folder',
"fp_eperm": 'cannot paste:\nyou do not have “write” permission in this folder',
"fr_emore": "select at least one item to rename",
"fd_emore": "select at least one item to delete",
"fc_emore": "select at least one item to cut",
"fs_sc": "share the folder you're in",
"fs_ss": "share the selected files",
"fs_just1d": "you cannot select more than one folder,\nor mix files and folders in one selection",
"fs_abrt": "❌ abort",
"fs_rand": "🎲 rand.name",
"fs_go": "✅ create share",
"fs_name": "name",
"fs_src": "source",
"fs_pwd": "passwd",
"fs_exp": "expiry",
"fs_tmin": "min",
"fs_thrs": "hours",
"fs_tdays": "days",
"fs_never": "eternal",
"fs_pname": "optional link name; will be random if blank",
"fs_tsrc": "the file or folder to share",
"fs_ppwd": "optional password",
"fs_w8": "creating share...",
"fs_ok": "press <code>Enter/OK</code> to Clipboard\npress <code>ESC/Cancel</code> to Close",
"frt_dec": "may fix some cases of broken filenames\">url-decode",
"frt_rst": "reset modified filenames back to the original ones\">↺ reset",
"frt_abrt": "abort and close this window\">❌ cancel",
"frb_apply": "APPLY RENAME",
"fr_adv": "batch / metadata / pattern renaming\">advanced",
"fr_case": "case-sensitive regex\">case",
"fr_win": "windows-safe names; replace <code>&lt;&gt;:&quot;\\|?*</code> with japanese fullwidth characters\">win",
"fr_slash": "replace <code>/</code> with a character that doesn't cause new folders to be created\">no /",
"fr_re": "regex search pattern to apply to original filenames; capturing groups can be referenced in the format field below like &lt;code&gt;(1)&lt;/code&gt; and &lt;code&gt;(2)&lt;/code&gt; and so on",
"fr_fmt": "inspired by foobar2000:$N&lt;code&gt;(title)&lt;/code&gt; is replaced by song title,$N&lt;code&gt;[(artist) - ](title)&lt;/code&gt; skips [this] part if artist is blank$N&lt;code&gt;$lpad((tn),2,0)&lt;/code&gt; pads tracknumber to 2 digits",
"fr_pdel": "delete",
"fr_pnew": "save as",
"fr_pname": "provide a name for your new preset",
"fr_aborted": "aborted",
"fr_lold": "old name",
"fr_lnew": "new name",
"fr_tags": "tags for the selected files (read-only, just for reference):",
"fr_busy": "renaming {0} items...\n\n{1}",
"fr_efail": "rename failed:\n",
"fr_nchg": "{0} of the new names were altered due to <code>win</code> and/or <code>no /</code>\n\nOK to continue with these altered new names?",
"fd_ok": "delete OK",
"fd_err": "delete failed:\n",
"fd_none": "nothing was deleted; maybe blocked by server config (xbd)?",
"fd_busy": "deleting {0} items...\n\n{1}",
"fd_warn1": "DELETE these {0} items?",
"fd_warn2": "<b>Last chance!</b> No way to undo. Delete?",
"fc_ok": "cut {0} items",
"fc_warn": 'cut {0} items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)',
"fp_ecut": "first cut some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs",
"fp_ename": "these {0} items cannot be moved here (names already exist):",
"fp_ok": "move OK",
"fp_busy": "moving {0} items...\n\n{1}",
"fp_err": "move failed:\n",
"fp_confirm": "move these {0} items here?",
"fp_etab": 'failed to read clipboard from other browser tab',
"fp_name": "uploading a file from your device. Give it a name:",
"fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device',
"fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>',
"mk_noname": "type a name into the text field on the left before you do that :p",
"tv_load": "Loading text document:\n\n{0}\n\n{1}% ({2} of {3} MiB loaded)",
"tv_xe1": "could not load textfile:\n\nerror ",
"tv_xe2": "404, file not found",
"tv_lst": "list of textfiles in",
"tvt_close": "return to folder view$NHotkey: M (or Esc)\">❌ close",
"tvt_dl": "download this file$NHotkey: Y\">💾 download",
"tvt_prev": "show previous document$NHotkey: i\">⬆ prev",
"tvt_next": "show next document$NHotkey: K\">⬇ next",
"tvt_sel": "select file &nbsp; ( for cut / delete / ... )$NHotkey: S\">sel",
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
"gt_vau": "don't show videos, just play the audio\">🎧",
"gt_msel": "enable file selection; ctrl-click a file to override$N$N&lt;em&gt;when active: doubleclick a file / folder to open it&lt;/em&gt;$N$NHotkey: S\">multiselect",
"gt_crop": "center-crop thumbnails\">crop",
"gt_3x": "hi-res thumbnails\">3x",
"gt_zoom": "zoom",
"gt_chop": "chop",
"gt_sort": "sort by",
"gt_name": "name",
"gt_sz": "size",
"gt_ts": "date",
"gt_ext": "type",
"gt_c1": "truncate filenames more (show less)",
"gt_c2": "truncate filenames less (show more)",
"sm_w8": "searching...",
"sm_prev": "search results below are from a previous query:\n ",
"sl_close": "close search results",
"sl_hits": "showing {0} hits",
"sl_moar": "load more",
"s_sz": "size",
"s_dt": "date",
"s_rd": "path",
"s_fn": "name",
"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": "uploaded after",
"s_u2": "and/or before",
"s_r1": "path contains &nbsp; (space-separated)",
"s_f1": "name contains &nbsp; (negate with -nope)",
"s_t1": "tags contains &nbsp; (^=start, end=$)",
"s_a1": "specific metadata properties",
"md_eshow": "cannot render ",
"md_off": "[📜<em>readme</em>] disabled in [⚙️] -- document hidden",
"badreply": "Failed to parse reply from server",
"xhr403": "403: Access denied\n\ntry pressing F5, maybe you got logged out",
"xhr0": "unknown (probably lost connection to server, or server is offline)",
"cf_ok": "sorry about that -- DD" + wah + "oS protection kicked in\n\nthings should resume in about 30 sec\n\nif nothing happens, hit F5 to reload the page",
"tl_xe1": "could not list subfolders:\n\nerror ",
"tl_xe2": "404: Folder not found",
"fl_xe1": "could not list files in folder:\n\nerror ",
"fl_xe2": "404: Folder not found",
"fd_xe1": "could not create subfolder:\n\nerror ",
"fd_xe2": "404: Parent folder not found",
"fsm_xe1": "could not send message:\n\nerror ",
"fsm_xe2": "404: Parent folder not found",
"fu_xe1": "failed to load unpost list from server:\n\nerror ",
"fu_xe2": "404: File not found??",
"fz_tar": "uncompressed gnu-tar file (linux / mac)",
"fz_pax": "uncompressed pax-format tar (slower)",
"fz_targz": "gnu-tar with gzip level 3 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead",
"fz_tarxz": "gnu-tar with xz level 1 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead",
"fz_zip8": "zip with utf8 filenames (maybe wonky on windows 7 and older)",
"fz_zipd": "zip with traditional cp437 filenames, for really old software",
"fz_zipc": "cp437 with crc32 computed early,$Nfor MS-DOS PKZIP v2.04g (october 1993)$N(takes longer to process before download can start)",
"un_m1": "you can delete your recent uploads (or abort unfinished ones) below",
"un_upd": "refresh",
"un_m4": "or share the files visible below:",
"un_ulist": "show",
"un_ucopy": "copy",
"un_flt": "optional filter:&nbsp; URL must contain",
"un_fclr": "clear filter",
"un_derr": 'unpost-delete failed:\n',
"un_f5": 'something broke, please try a refresh or hit F5',
"un_uf5": "sorry but you have to refresh the page (for example by pressing F5 or CTRL-R) before this upload can be aborted",
"un_nou": '<b>warning:</b> server too busy to show unfinished uploads; click the "refresh" link in a bit',
"un_noc": '<b>warning:</b> unpost of fully uploaded files is not enabled/permitted in server config',
"un_max": "showing first 2000 files (use the filter)",
"un_avail": "{0} recent uploads can be deleted<br />{1} unfinished ones can be aborted",
"un_m2": "sorted by upload time; most recent first:",
"un_no1": "sike! no uploads are sufficiently recent",
"un_no2": "sike! no uploads matching that filter are sufficiently recent",
"un_next": "delete the next {0} files below",
"un_abrt": "abort",
"un_del": "delete",
"un_m3": "loading your recent uploads...",
"un_busy": "deleting {0} files...",
"un_clip": "{0} links copied to clipboard",
"u_https1": "you should",
"u_https2": "switch to https",
"u_https3": "for better performance",
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>',
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",
"u_enpot": 'switch to <a href="#">potato UI</a> (may improve upload speed)',
"u_depot": 'switch to <a href="#">fancy UI</a> (may reduce upload speed)',
"u_gotpot": 'switching to the potato UI for improved upload speed,\n\nfeel free to disagree and switch back!',
"u_pott": "<p>files: &nbsp; <b>{0}</b> finished, &nbsp; <b>{1}</b> failed, &nbsp; <b>{2}</b> busy, &nbsp; <b>{3}</b> queued</p>",
"u_ever": "this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
"u_su2k": 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better',
"u_ewrite": 'you do not have write-access to this folder',
"u_eread": 'you do not have read-access to this folder',
"u_enoi": 'file-search is not enabled in server config',
"u_badf": 'These {0} files (of {1} total) were skipped, possibly due to filesystem permissions:\n\n',
"u_blankf": 'These {0} files (of {1} total) are blank / empty; upload them anyways?\n\n',
"u_just1": '\nMaybe it works better if you select just one file',
"u_ff_many": "if you're using <b>Linux / MacOS / Android,</b> then this amount of files <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>may</em> crash Firefox!</a>\nif that happens, please try again (or use Chrome).",
"u_up_life": "This upload will be deleted from the server\n{0} after it completes",
"u_asku": 'upload these {0} files to <code>{1}</code>',
"u_unpt": "you can undo / delete this upload using the top-left 🧯",
"u_bigtab": 'about to show {0} files\n\nthis may crash your browser, are you sure?',
"u_scan": 'Scanning files...',
"u_dirstuck": 'directory iterator got stuck trying to access the following {0} items; will skip:',
"u_etadone": 'Done ({0}, {1} files)',
"u_etaprep": '(preparing to upload)',
"u_hashdone": 'hashing done',
"u_hashing": 'hash',
"u_hs": 'handshaking...',
"u_dupdefer": "duplicate; will be processed after all other files",
"u_actx": "click this text to prevent loss of<br />performance when switching to other windows/tabs",
"u_fixed": "OK!&nbsp; Fixed it 👍",
"u_cuerr": "failed to upload chunk {0} of {1};\nprobably harmless, continuing\n\nfile: {2}",
"u_cuerr2": "server rejected upload (chunk {0} of {1});\nwill retry later\n\nfile: {2}\n\nerror ",
"u_ehstmp": "will retry; see bottom-right",
"u_ehsfin": "server rejected the request to finalize upload; retrying...",
"u_ehssrch": "server rejected the request to perform search; retrying...",
"u_ehsinit": "server rejected the request to initiate upload; retrying...",
"u_eneths": "network error while performing upload handshake; retrying...",
"u_enethd": "network error while testing target existence; retrying...",
"u_cbusy": "waiting for server to trust us again after a network glitch...",
"u_ehsdf": "server ran out of disk space!\n\nwill keep retrying, in case someone\nfrees up enough space to continue",
"u_emtleak1": "it looks like your webbrowser may have a memory leak;\nplease",
"u_emtleak2": ' <a href="{0}">switch to https (recommended)</a> or ',
"u_emtleak3": ' ',
"u_emtleakc": 'try the following:\n<ul><li>hit <code>F5</code> to refresh the page</li><li>then disable the &nbsp;<code>mt</code>&nbsp; button in the &nbsp;<code>⚙️ settings</code></li><li>and try that upload again</li></ul>Uploads will be a bit slower, but oh well.\nSorry for the trouble !\n\nPS: chrome v107 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1354816" target="_blank">has a bugfix</a> for this',
"u_emtleakf": 'try the following:\n<ul><li>hit <code>F5</code> to refresh the page</li><li>then enable <code>🥔</code> (potato) in the upload UI<li>and try that upload again</li></ul>\nPS: firefox <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1790500" target="_blank">will hopefully have a bugfix</a> at some point',
"u_s404": "not found on server",
"u_expl": "explain",
"u_maxconn": "most browsers limit this to 6, but firefox lets you raise it with <code>connections-per-server</code> in <code>about:config</code>",
"u_tu": '<p class="warn">WARNING: turbo enabled, <span>&nbsp;client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>',
"u_ts": '<p class="warn">WARNING: turbo enabled, <span>&nbsp;search results can be incorrect; see turbo-button tooltip</span></p>',
"u_turbo_c": "turbo is disabled in server config",
"u_turbo_g": "disabling turbo because you don't have\ndirectory listing privileges within this volume",
"u_life_cfg": 'autodelete after <input id="lifem" p="60" /> min (or <input id="lifeh" p="3600" /> hours)',
"u_life_est": 'upload will be deleted <span id="lifew" tt="local time">---</span>',
"u_life_max": 'this folder enforces a\nmax lifetime of {0}',
"u_unp_ok": 'unpost is allowed for {0}',
"u_unp_ng": 'unpost will NOT be allowed',
"ue_ro": 'your access to this folder is Read-Only\n\n',
"ue_nl": 'you are currently not logged in',
"ue_la": 'you are currently logged in as "{0}"',
"ue_sr": 'you are currently in file-search mode\n\nswitch to upload-mode by clicking the magnifying glass 🔎 (next to the big SEARCH button), and try uploading again\n\nsorry',
"ue_ta": 'try uploading again, it should work now',
"ur_1uo": "OK: File uploaded successfully",
"ur_auo": "OK: All {0} files uploaded successfully",
"ur_1so": "OK: File found on server",
"ur_aso": "OK: All {0} files found on server",
"ur_1un": "Upload failed, sorry",
"ur_aun": "All {0} uploads failed, sorry",
"ur_1sn": "File was NOT found on server",
"ur_asn": "The {0} files were NOT found on server",
"ur_um": "Finished;\n{0} uploads OK,\n{1} uploads failed, sorry",
"ur_sm": "Finished;\n{0} files found on server,\n{1} files NOT found on server",
"lang_set": "refresh to make the change take effect?",
},
};

184
scripts/tl.py Executable file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env python3
import os
import sys
"""
generates a "tl.js" which can be used to prototype a new translation
without having to run from source, or tamper with the files that are
extracted from the sfx
to generate a tl.js for the language you are translating to,
run the following command: python tl.py fra Français
("fra" being the three-letter language code of your language, and
"Français" the native name of the language)
then copy tl.js into the webroot and run copyparty with the following:
on Linux: --html-head='<script src="/tl.js"></script>'
on Windows: "--html-head=<script src='/tl.js'></script>"
or in a copyparty config file:
[global]
html-head: <script src="/tl.js"></script>
after editing tl.js, reload your webbrowser by pressing ctrl-shift-r
"""
#######################################################################
#######################################################################
def generate_javascript(lang3, native_name, tl_browser):
note = "// NOTE: please use tabs for indenting, not spaces :-)"
if lang3 == "eng":
note += """\n
// NOTE: since you are using the tl.js straight from the repo, you'll
// need to find/replace all "eng" with your own three-letter name"""
return f""""use strict";
{note}
// the three-letter name of the language you're translating to
var my_lang = "{lang3}";
// and because we don't know these yet...
var SR='', wah='';
// this function is automatically called when the page is loaded:
function langmod() {{
// which page is the javascript currently running on?
// look for some well-known elements to figure it out:
if (QS("h1#cc") && QS("a#k")) {{
// we are running in the control-panel
Ls[my_lang] = tl_cpanel[my_lang];
}}
else if (ebi("op_cfg")) {{
// we are running in the filebrowser
Ls[my_lang] = tl_browser[my_lang];
// inform the settings tab that a new lang is available
LANGS.push(my_lang);
}}
}}
////////////////////////////////////////////////////////////////////////
// below this point is where the actual translation happens;
// here is the pairs of "text-identifier": "text-to-translate"
////////////////////////////////////////////////////////////////////////
// translation of splash.js (the control-panel);
// you do not need to translate the TLNotes, those are just for you :-)
var tl_cpanel = {{
"{lang3}": {{
"a1": "refresh",
"b1": "howdy stranger &nbsp; <small>(you're not logged in)</small>",
"c1": "logout",
"d1": "dump stack", // TLNote: "d2" is the tooltip for this button
"d2": "shows the state of all active threads",
"e1": "reload cfg",
"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",
"f1": "you can browse:",
"g1": "you can upload to:",
"cc1": "other stuff:",
"h1": "disable k304", // TLNote: "j1" explains what k304 is
"i1": "enable k304",
"j1": "enabling this 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",
"k1": "reset client settings",
"l1": "login for more:",
"m1": "welcome back,", // TLNote: "welcome back, USERNAME"
"n1": "404 not found &nbsp;┐( ´ -`)┌",
"o1": 'or maybe you don\\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
"p1": "403 forbiddena &nbsp;~┻━┻",
"q1": 'use a password or <a href="' + SR + '/?h">go home</a>',
"r1": "go home",
".s1": "rescan",
"t1": "action", // TLNote: this is the header above the "rescan" buttons
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v1": "connect",
"v2": "use this server as a local HDD",
"w1": "switch to https",
"x1": "change password",
"y1": "edit shares", // TLNote: shows the list of folders that the user has decided to share
"z1": "unlock this share:", // TLNote: the password prompt to see a hidden share
"ta1": "fill in your new password first",
"ta2": "repeat to confirm new password:",
"ta3": "found a typo; please try again",
"aa1": "incoming files:",
}},
}};
////////////////////////////////////////////////////////////////////////
// translation of browser.js (the filebrowser):
var tl_browser = {{
"{lang3}": {{
"tt": "{native_name}",
{tl_browser}
}};
"""
#######################################################################
#######################################################################
def die(*a):
print(*a)
sys.exit(1)
def main():
webdir = "../copyparty/web/splash.js"
while webdir and not os.path.exists(webdir):
webdir = webdir.split("/", 1)[1]
if not webdir:
t = "could not find the copyparty/web/*.js files!\nplease cd into the copyparty repo before running this script"
die(t)
webdir = webdir.rsplit("/", 1)[0] if "/" in webdir else "."
with open(os.path.join(webdir, "browser.js"), "rb") as f:
browserjs = f.read().decode("utf-8")
_, browserjs = browserjs.split('\n\t\t"tt": "English",\n', 1)
browserjs, _ = browserjs.split('\n\t"nor": {', 1)
try:
lang3 = sys.argv[1]
except:
t = "you need to provide one more argument: the three-letter language code for the language you are translating to, for example: ger"
die(t)
try:
native_name = sys.argv[2]
except:
t = "you need to provide one more argument: the native name of the language you are translating to, for example: Deutsch"
die(t)
ret = generate_javascript(lang3, native_name, browserjs)
outpath = os.path.abspath("tl.js")
if os.path.exists(outpath):
t = "the output file already exists! if you really want to overwrite it, then delete the following file and try again:"
die(t, outpath)
with open(outpath, "wb") as f:
f.write(ret.encode("utf-8"))
print("successfully created", outpath)
if __name__ == "__main__":
main()

View File

@@ -22,8 +22,12 @@ awk -v apos=\' -v quot=\" '
!$0 && t!=tp {
print "\n\033[1;37;41m====DIFF===="
}
!$0 && s==sp {
print "\n\033[1;37;44m====IDENTICAL===="
}
!$0 { print; next; }
{
sp=s; s=$0;
tp=t; t="";
c(quot);
c(apos);

View File

@@ -20,7 +20,7 @@ cat $f | awk '
o{next}
/^#/{s=1;rs=0;pr()}
/^#* *(nix package)/{rs=1}
/^#* *(install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module)|`$/{s=rs}
/^#* *(themes|install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module)|```/{s=rs}
/^#/{
lv=length($1);
sub(/[^ ]+ /,"");

View File

@@ -1,11 +1,6 @@
#!/usr/bin/env python3
import atexit
import os
import platform
import sys
import tarfile
import tempfile
import time
import traceback
@@ -23,20 +18,6 @@ def msg(*a, **ka):
print(*a, **ka)
def utime(top):
# avoid cleaners
files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
try:
while True:
t = int(time.time())
for f in [top] + files:
os.utime(f, (t, t))
time.sleep(78123)
except Exception as ex:
print("utime:", ex, f)
def confirm(rv):
msg()
msg("retcode", rv if rv else traceback.format_exc())
@@ -51,47 +32,17 @@ def confirm(rv):
def run():
import copyparty
from copyparty.__main__ import main as cm
td = tempfile.TemporaryDirectory(prefix="")
atexit.register(td.cleanup)
rsrc = td.name
try:
from importlib.resources import files
f = files(copyparty).joinpath("z.tar").open("rb")
except:
from importlib.resources import open_binary
f = open_binary("copyparty", "z.tar")
with tarfile.open(fileobj=f) as tf:
try:
tf.extractall(rsrc, filter="tar")
except TypeError:
tf.extractall(rsrc) # nosec (archive is safe)
f.close()
f = None
msg(" rsrc dir:", rsrc)
msg()
sys.argv.append("--sfx-tpoke=" + rsrc)
cm(rsrc=rsrc)
cm()
def main():
sysver = str(sys.version).replace("\n", "\n" + " " * 18)
pktime = time.strftime("%Y-%m-%d, %H:%M:%S", time.gmtime(STAMP))
msg()
msg(" this is: copyparty", VER)
msg(" packed at:", pktime, "UTC,", STAMP)
msg("python bin:", sys.executable)
msg("python ver:", platform.python_implementation(), sysver)
msg("build-time:", pktime, "UTC,", STAMP)
msg("python-bin:", sys.executable)
msg()
try:
run()

View File

@@ -19,6 +19,20 @@ class TestDedup(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
# (data, chash, wark)
self.files = [
(
"one",
"BfcDQQeKz2oG1CPSFyD5ZD1flTYm2IoCY23DqeeVgq6w",
"XMbpLRqVdtGmgggqjUI6uSoNMTqZVX4K6zr74XA1BRKc",
),
(
"two",
"ko1Q0eJNq3zKYs_oT83Pn8aVFgonj5G1wK8itwnYL4qj",
"fxvihWlnQIbVbUPr--TxyV41913kPLhXPD1ngXYxDfou",
),
]
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
@@ -31,54 +45,138 @@ class TestDedup(unittest.TestCase):
os.chdir(td)
return td
def cinit(self):
if self.conn:
self.fstab = self.conn.hsrv.hub.up2k.fstab
self.conn.hsrv.hub.up2k.shutdown()
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True)
if self.fstab:
self.conn.hsrv.hub.up2k.fstab = self.fstab
def test_a(self):
file404 = "\nJ2EOT"
f1, f2 = self.files
fns = ("f1", "f2", "f3")
dn = "d"
self.conn = None
self.fstab = None
for e2d in [True, False]:
self.args = Cfg(v=[".::A"], a=[], e2d=e2d)
self.reset()
self.cinit()
# dupes in parallel
sfn, hs = self.do_post_hs(dn, fns[0], f1, True)
for fn in fns[1:]:
h, b = self.handshake(dn, fn, f1)
self.assertIn(" 422 Unpro", h)
self.assertIn("a different location;", b)
self.do_post_data(dn, fns[0], f1, True, sfn, hs)
if not e2d:
# dupesched is e2d only; hs into existence
for fn, data in zip(fns, (f1[0], file404, file404)):
h, b = self.curl("%s/%s" % ("d", fn))
self.assertEqual(b, data)
for fn in fns[1:]:
h, b = self.do_post_hs(dn, fn, f1, False)
for fn in fns:
h, b = self.curl("%s/%s" % ("d", fn))
self.assertEqual(b, f1[0])
if not e2d:
continue
# overwrite file
sfn, hs = self.do_post_hs(dn, fns[0], f2, True, replace=True)
self.do_post_data(dn, fns[0], f2, True, sfn, hs)
for fn, f in zip(fns, (f2, f1, f1)):
h, b = self.curl("%s/%s" % ("d", fn))
self.assertEqual(b, f[0])
def test(self):
quick = True # sufficient for regular smoketests
# quick = False
dirnames = ["d1", "d2"]
filenames = ["f1", "f2"]
files = [
(
"one",
"BfcDQQeKz2oG1CPSFyD5ZD1flTYm2IoCY23DqeeVgq6w",
"XMbpLRqVdtGmgggqjUI6uSoNMTqZVX4K6zr74XA1BRKc",
),
(
"two",
"ko1Q0eJNq3zKYs_oT83Pn8aVFgonj5G1wK8itwnYL4qj",
"fxvihWlnQIbVbUPr--TxyV41913kPLhXPD1ngXYxDfou",
),
]
# (data, chash, wark)
files = self.files
# 3072 uploads in total
self.ctr = 3072
self.ctr = 336 if quick else 2016 # estimated total num uploads
self.conn = None
self.fstab = None
for e2d in [True, False]:
for dn1, fn1, f1 in product(dirnames, filenames, files):
for dn2, fn2, f2 in product(dirnames, filenames, files):
for dn3, fn3, f3 in product(dirnames, filenames, files):
self.reset()
if self.conn:
self.conn.hsrv.hub.up2k.shutdown()
self.args = Cfg(v=[".::A"], a=[], e2d=e2d)
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(
self.args, self.asrv, self.log, b"", True
)
self.do_post(dn1, fn1, f1, True)
self.do_post(dn2, fn2, f2, False)
self.do_post(dn3, fn3, f3, False)
self.args = Cfg(v=[".::A"], a=[], e2d=e2d)
for cm1 in product(dirnames, filenames, files):
for cm2 in product(dirnames, filenames, files):
if cm1 == cm2:
continue
for cm3 in product(dirnames, filenames, files):
if cm3 in (cm1, cm2):
continue
f1 = cm1[2]
f2 = cm2[2]
f3 = cm3[2]
if not e2d:
rms = [-1]
elif f1 == f2:
if f1 == f3:
rms = [0, 1, 2]
else:
rms = [0, 1]
elif f1 == f3:
rms = [0, 2]
else:
rms = [1, 2]
for rm in rms:
self.do_tc(cm1, cm2, cm3, rm)
if quick:
break
def do_tc(self, cm1, cm2, cm3, irm):
dn1, fn1, f1 = cm1
dn2, fn2, f2 = cm2
dn3, fn3, f3 = cm3
self.reset()
self.cinit()
fn1 = self.do_post(dn1, fn1, f1, True)
fn2 = self.do_post(dn2, fn2, f2, False)
fn3 = self.do_post(dn3, fn3, f3, False)
if irm < 0:
return
cms = [(dn1, fn1, f1), (dn2, fn2, f2), (dn3, fn3, f3)]
rm = cms[irm]
dn, fn, _ = rm
h, b = self.curl("%s/%s?delete" % (dn, fn), meth="POST")
self.assertIn(" 200 OK", h)
self.assertIn("deleted 1 files", b)
h, b = self.curl("%s/%s" % (dn, fn))
self.assertIn(" 404 Not Fo", h)
for cm in cms:
if cm == rm:
continue
dn, fn, f = cm
h, b = self.curl("%s/%s" % (dn, fn))
self.assertEqual(b, f[0])
def do_post(self, dn, fn, fi, first):
print("\n\n# do_post", self.ctr, repr((dn, fn, fi, first)))
self.ctr -= 1
sfn, hs = self.do_post_hs(dn, fn, fi, first)
return self.do_post_data(dn, fn, fi, first, sfn, hs)
data, chash, wark = fi
hs = self.handshake(dn, fn, fi)
self.assertEqual(hs["wark"], wark)
def do_post_hs(self, dn, fn, fi, first, replace=False):
h, b = self.handshake(dn, fn, fi, replace=replace)
hs = json.loads(b)
self.assertEqual(hs["wark"], fi[2])
sfn = hs["name"]
if sfn == fn:
@@ -88,6 +186,10 @@ class TestDedup(unittest.TestCase):
if first:
raise Exception("wait what")
return sfn, hs
def do_post_data(self, dn, fn, fi, first, sfn, hs):
data, chash, wark = fi
if hs["hash"]:
self.assertEqual(hs["hash"][0], chash)
self.put_chunk(dn, wark, chash, data)
@@ -96,17 +198,20 @@ class TestDedup(unittest.TestCase):
h, b = self.curl("%s/%s" % (dn, sfn))
self.assertEqual(b, data)
return sfn
def handshake(self, dn, fn, fi):
def handshake(self, dn, fn, fi, replace=False):
hdr = "POST /%s/ HTTP/1.1\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n"
msg = {"name": fn, "size": 3, "lmod": 1234567890, "life": 0, "hash": [fi[1]]}
if replace:
msg["replace"] = True
buf = json.dumps(msg).encode("utf-8")
buf = (hdr % (dn, len(buf))).encode("utf-8") + buf
print("HS -->", buf)
# print("HS -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("HS <--", ret)
return json.loads(ret[1])
# print("HS <--", ret)
return ret
def put_chunk(self, dn, wark, chash, data):
msg = [
@@ -120,14 +225,16 @@ class TestDedup(unittest.TestCase):
data,
]
buf = "\r\n".join(msg).encode("utf-8")
print("PUT -->", buf)
# print("PUT -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
self.assertEqual(ret[1], "thank")
def curl(self, url, binary=False):
h = "GET /%s HTTP/1.1\r\nConnection: close\r\n\r\n"
HttpCli(self.conn.setbuf((h % (url,)).encode("utf-8"))).run()
def curl(self, url, binary=False, meth=None):
h = "%s /%s HTTP/1.1\r\nConnection: close\r\n\r\n"
h = h % (meth or "GET", url)
# print("CURL -->", url)
HttpCli(self.conn.setbuf(h.encode("utf-8"))).run()
if binary:
h, b = self.conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]

View File

@@ -107,9 +107,9 @@ class TestDots(unittest.TestCase):
os.rename(".b", "v/.b")
vcfg = [
".::r.,u1:g,u2:c,dk",
"v/a:v/a:r.,u1:g,u2:c,dk",
"v/.b:v/.b:r.,u1:g,u2:c,dk",
".::r.,u1:g,u2:c,dks",
"v/a:v/a:r.,u1:g,u2:c,dks",
"v/.b:v/.b:r.,u1:g,u2:c,dks",
]
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"])
self.asrv = AuthSrv(self.args, self.log)

View File

@@ -26,6 +26,7 @@ def hdr(query):
class TestHttpCli(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
self.maxDiff = 99999
def tearDown(self):
os.chdir(tempfile.gettempdir())
@@ -39,6 +40,7 @@ class TestHttpCli(unittest.TestCase):
os.mkdir(td)
os.chdir(td)
# "perm+user"; r/w/a (a=rw) for user a/o/x (a=all)
self.dtypes = ["ra", "ro", "rx", "wa", "wo", "wx", "aa", "ao", "ax"]
self.can_read = ["ra", "ro", "aa", "ao"]
self.can_write = ["wa", "wo", "aa", "ao"]
@@ -86,6 +88,7 @@ class TestHttpCli(unittest.TestCase):
self.args = Cfg(v=vcfg, a=["o:o", "x:x"])
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"")
vfiles = [x for x in allfiles if x.startswith(top)]
for fp in vfiles:
tctr += 1
@@ -120,7 +123,8 @@ class TestHttpCli(unittest.TestCase):
# expected files in archives
if rok:
ref = [x for x in vfiles if self.in_dive(top + "/" + durl, x)]
zs = top + "/" + durl
ref = [x for x in vfiles if self.in_dive(zs, x)]
ref.sort()
else:
ref = []
@@ -165,7 +169,7 @@ class TestHttpCli(unittest.TestCase):
self.assertEqual([], zf_ng)
# stash
h, ret = self.put(url)
h, ret = self.put(durl)
res = h.startswith("HTTP/1.1 201 ")
self.assertEqual(res, wok)
if wok:
@@ -204,14 +208,14 @@ class TestHttpCli(unittest.TestCase):
buf = "PUT /{0} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\nContent-Length: {1}\r\n\r\nok {0}\n"
buf = buf.format(url, len(url) + 4).encode("utf-8")
print("PUT -->", buf)
conn = tu.VHttpConn(self.args, self.asrv, self.log, buf)
conn = self.conn.setbuf(buf)
HttpCli(conn).run()
ret = conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("PUT <--", ret)
return ret
def curl(self, url, binary=False):
conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url))
conn = self.conn.setbuf(hdr(url))
HttpCli(conn).run()
if binary:
h, b = conn.s._reply.split(b"\r\n\r\n", 1)

104
tests/test_metrics.py Normal file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
import re
import shutil
import tempfile
import unittest
from copyparty.__init__ import PY2
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from copyparty.metrics import Metrics
from tests import util as tu
from tests.util import Cfg
def hdr(query):
h = "GET /{} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\n\r\n"
return h.format(query).encode("utf-8")
class TestMetrics(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
os.chdir(self.td)
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def cinit(self):
if self.conn:
self.fstab = self.conn.hsrv.hub.up2k.fstab
self.conn.hsrv.hub.up2k.shutdown()
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True)
if self.fstab:
self.conn.hsrv.hub.up2k.fstab = self.fstab
if not self.metrics:
self.metrics = Metrics(self.conn)
self.conn.broker = self.conn.hsrv.broker
self.conn.hsrv.metrics = self.metrics
for k in "ncli nsus nban".split():
setattr(self.conn, k, 9)
def test(self):
zs = "nos_dup nos_hdd nos_unf nos_vol nos_vst"
opts = {x: False for x in zs.split()}
self.args = Cfg(v=[".::A"], a=[], stats=True, **opts)
self.conn = self.fstab = self.metrics = None
self.cinit()
h, b = self.curl(".cpr/metrics")
self.assertIn(".1 200 OK", h)
ptns = r"""
cpp_uptime_seconds [0-9]\.[0-9]{3}$
cpp_boot_unixtime_seconds [0-9]{7,10}\.[0-9]{3}$
cpp_http_reqs_created [0-9]{7,10}$
cpp_http_reqs_total -1$
cpp_http_conns 9$
cpp_total_bans 9$
cpp_sus_reqs_total 9$
cpp_active_bans 0$
cpp_idle_vols 0$
cpp_busy_vols 0$
cpp_offline_vols 0$
cpp_db_idle_seconds 86399999\.00$
cpp_db_act_seconds 0\.00$
cpp_hashing_files 0$
cpp_tagq_files 0$
cpp_disk_size_bytes\{vol="/"\} [0-9]+$
cpp_disk_free_bytes\{vol="/"\} [0-9]+$
cpp_vol_bytes\{vol="/"\} 0$
cpp_vol_files\{vol="/"\} 0$
cpp_vol_bytes\{vol="total"\} 0$
cpp_vol_files\{vol="total"\} 0$
cpp_dupe_bytes\{vol="total"\} 0$
cpp_dupe_files\{vol="total"\} 0$
"""
if not PY2:
ptns += r"""
cpp_mtpq_files 0$
cpp_unf_bytes\{vol="/"\} 0$
cpp_unf_files\{vol="/"\} 0$
cpp_unf_bytes\{vol="total"\} 0$
cpp_unf_files\{vol="total"\} 0$
"""
for want in [x for x in ptns.split("\n") if x]:
if not re.search("^" + want, b.replace("\r", ""), re.MULTILINE):
raise Exception("server response:\n%s\n\nmissing item: %s" % (b, want))
def curl(self, url, binary=False):
conn = self.conn.setbuf(hdr(url))
HttpCli(conn).run()
if binary:
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def log(self, src, msg, c=0):
print(msg)

198
tests/test_mv.py Normal file
View File

@@ -0,0 +1,198 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import json
import os
import shutil
import tempfile
import unittest
from itertools import product
from copyparty.__init__ import PY2
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from tests import util as tu
from tests.util import Cfg
"""
TODO inject tags into db and verify ls
"""
class TestDedup(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def reset(self):
td = os.path.join(self.td, "vfs")
if os.path.exists(td):
shutil.rmtree(td)
os.mkdir(td)
os.chdir(td)
return td
def cinit(self):
if self.conn:
self.fstab = self.conn.hsrv.hub.up2k.fstab
self.conn.hsrv.hub.up2k.shutdown()
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True)
if self.fstab:
self.conn.hsrv.hub.up2k.fstab = self.fstab
def test(self):
if PY2:
raise unittest.SkipTest()
# tc_e2d = [True, False] # maybe-TODO only known symlinks are translated
tc_e2d = [True]
tc_dedup = ["sym", "no", "sym-no"]
tc_vols = [["::A"], ["::A", "d1:d1:A"]]
dirs = ["d1", "d1/d2", "d1/d2/d3", "d1/d4"]
files = [
(
"one",
"BfcDQQeKz2oG1CPSFyD5ZD1flTYm2IoCY23DqeeVgq6w",
"XMbpLRqVdtGmgggqjUI6uSoNMTqZVX4K6zr74XA1BRKc",
)
]
# (data, chash, wark)
self.conn = None
self.fstab = None
self.ctr = 0 # 2304
tcgen = product(tc_e2d, tc_dedup, tc_vols, dirs, ["d9", "../d9"])
for e2d, dedup, vols, mv_from, dst in tcgen:
if "/" not in mv_from and dst.startswith(".."):
continue # would move past top of fs
if len(vols) > 1 and mv_from == "d1":
continue # cannot move a vol
# print(e2d, dedup, vols, mv_from, dst)
ka = {"e2d": e2d}
if dedup == "hard":
ka["hardlink"] = True
elif dedup == "no":
ka["no_dedup"] = True
self.args = Cfg(v=vols[:], a=[], **ka)
for u1, u2, u3, u4 in product(dirs, dirs, dirs, dirs):
ups = (u1, u2, u3, u4)
if len(set(ups)) < 4:
continue # not unique
t = "e2d:%s dedup:%s vols:%d from:%s to:%s"
t = t % (e2d, dedup, len(vols), mv_from, dst)
print("\n\n\033[0;7m# files:", ups, t, "\033[0m")
self.reset()
self.cinit()
for up in [u1, u2, u3, u4]:
self.do_post(up, "fn", files[0], up == u1)
restore_args = None
if dedup == "sym-no":
restore_args = self.args
ka = {"e2d": e2d, "no_dedup": True}
self.args = Cfg(v=vols[:], a=[], **ka)
self.cinit()
mv_to = mv_from
for _ in range(2 if dst.startswith("../") else 1):
mv_to = mv_from.rsplit("/", 1)[0] if "/" in mv_from else ""
mv_to += "/" + dst.lstrip("./")
self.do_mv(mv_from, mv_to)
for dirpath in [u1, u2, u3, u4]:
if dirpath == mv_from:
dirpath = mv_to
elif dirpath.startswith(mv_from):
dirpath = mv_to + dirpath[len(mv_from) :]
h, b = self.curl(dirpath + "/fn")
self.assertEqual(b, "one")
if restore_args:
self.args = restore_args
def do_mv(self, src, dst):
hdr = "POST /%s?move=/%s HTTP/1.1\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
buf = (hdr % (src, dst)).encode("utf-8")
print("MV [%s] => [%s]" % (src, dst))
HttpCli(self.conn.setbuf(buf)).run()
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("MV <-- ", ret)
self.assertIn(" 201 Created", ret[0])
self.assertEqual("k\r\n", ret[1])
return ret
def do_post(self, dn, fn, fi, first):
print("\n# do_post", self.ctr, repr((dn, fn, fi, first)))
self.ctr -= 1
data, chash, wark = fi
hs = self.handshake(dn, fn, fi)
self.assertEqual(hs["wark"], wark)
sfn = hs["name"]
if sfn == fn:
print("using original name " + fn)
else:
print(fn + " got renamed to " + sfn)
if first:
raise Exception("wait what")
if hs["hash"]:
self.assertEqual(hs["hash"][0], chash)
self.put_chunk(dn, wark, chash, data)
elif first:
raise Exception("found first; %r, %r" % ((dn, fn, fi), hs))
h, b = self.curl("%s/%s" % (dn, sfn))
self.assertEqual(b, data)
def handshake(self, dn, fn, fi):
hdr = "POST /%s/ HTTP/1.1\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n"
msg = {"name": fn, "size": 3, "lmod": 1234567890, "life": 0, "hash": [fi[1]]}
buf = json.dumps(msg).encode("utf-8")
buf = (hdr % (dn, len(buf))).encode("utf-8") + buf
print("HS -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("HS <--", ret)
return json.loads(ret[1])
def put_chunk(self, dn, wark, chash, data):
msg = [
"POST /%s/ HTTP/1.1" % (dn,),
"Connection: close",
"Content-Type: application/octet-stream",
"Content-Length: 3",
"X-Up2k-Hash: " + chash,
"X-Up2k-Wark: " + wark,
"",
data,
]
buf = "\r\n".join(msg).encode("utf-8")
print("PUT -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
self.assertEqual(ret[1], "thank")
def curl(self, url, binary=False):
h = "GET /%s HTTP/1.1\r\nConnection: close\r\n\r\n"
HttpCli(self.conn.setbuf((h % (url,)).encode("utf-8"))).run()
if binary:
h, b = self.conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def log(self, src, msg, c=0):
print(msg)

38
tests/test_utils.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import unittest
from copyparty.__main__ import PY2
from copyparty.util import w8enc
from tests import util as tu
class TestUtils(unittest.TestCase):
def cmp(self, orig, t1, t2):
if t1 != t2:
raise Exception("\n%r\n%r\n%r\n" % (w8enc(orig), t1, t2))
def test_quotep(self):
if PY2:
raise unittest.SkipTest()
from copyparty.util import _quotep3, _quotep3b, w8dec
txt = w8dec(tu.randbytes(8192))
self.cmp(txt, _quotep3(txt), _quotep3b(txt))
def test_unquote(self):
if PY2:
raise unittest.SkipTest()
from urllib.parse import unquote_to_bytes as u2b
from copyparty.util import unquote
for btxt in (
tu.randbytes(8192),
br"%ed%91qw,er;ty%20as df?gh+jkl%zxc&vbn <qwe>\"rty'uio&asd&nbsp;fgh",
):
self.cmp(btxt, unquote(btxt), u2b(btxt))

View File

@@ -3,6 +3,7 @@
from __future__ import print_function, unicode_literals
import os
import random
import re
import shutil
import socket
@@ -49,6 +50,10 @@ from copyparty.util import FHC, CachedDict, Garda, Unrecv
init_E(E)
def randbytes(n):
return random.getrandbits(n * 8).to_bytes(n, "little")
def runcmd(argv):
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
stdout, stderr = p.communicate()
@@ -117,16 +122,16 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0):
ka = {}
ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_db_ip no_dedup no_del no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs"
ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_clone no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand re_dirsz smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs"
ka.update(**{k: False for k in ex.split()})
ex = "dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_snap no_voldump re_dhash plain_ip"
ex = "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 plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser hist js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
ex = "ah_cli ah_gen css_browser hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
ka.update(**{k: None for k in ex.split()})
ex = "hash_mt srch_time u2abort u2j u2sz"
ex = "hash_mt safe_dedup srch_time u2abort u2j u2sz"
ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol mtab_age reg_cap s_thead s_tbody th_convt"
@@ -262,6 +267,7 @@ class VHttpSrv(object):
self.u2idx = None
self.ptn_cc = re.compile(r"[\x00-\x1f]")
self.uparam_cc_ok = set("doc move tree".split())
def cachebuster(self):
return "a"