Compare commits

...

75 Commits

Author SHA1 Message Date
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
ed
abcdf479e6 v1.14.3 2024-08-30 23:11:22 +00:00
ed
ad2371f810 shares: add revival and expiration extension 2024-08-30 22:25:50 +00:00
ed
c4e2b0f95f doc-viewer: always wordwrap code 2024-08-30 22:13:10 +00:00
ed
3da62ec234 fix dedup bug as of v1.13.8:
* v1.13.8 broke collision resolving for non-identical files;
   the correct filename was reserved but not symlinked to
   the original file, leaving a zerobyte file instead.
   See v1.14.3 github release notes for remediation info

* add sanchecks for early detection of index/fs desync;
   saves performance and gives less confusing logs
2024-08-30 22:06:25 +00:00
ed
01233991f3 tftp: support unmapped root 2024-08-30 16:08:50 +00:00
ed
ee35974273 readme hacking 2024-08-29 22:17:13 +00:00
ed
7037e7365e add logo 2024-08-29 22:00:08 +00:00
ed
03b13e8a1c sfx-customizer:
* better translation stripping
* add support in bruteforcer
* add examples

and fix login-banner usage example
2024-08-28 05:53:26 +00:00
ed
cdd2da0208 update pkgs to 1.14.2 2024-08-23 23:43:46 +00:00
ed
cec0e0cf02 v1.14.2 2024-08-23 23:07:18 +00:00
ed
8122ddedfe share multiple files (#84);
if files (one or more) are selected for sharing, then
a virtual folder is created to hold the selected files

if a single file is selected for sharing, then
the returned URL will point directly to that file

and fix some shares-related bugs:
* password coalescing
* log-spam on reload
2024-08-23 22:55:31 +00:00
ultwcz
55a77c5e89 Chinese translation fixes (#95)
* fix: translation: changing from `" "` to `' '` for some strings;
	using `./scripts/tlcheck.sh eng chi copyparty/web/browser.js`

* fix: translation: Check the newly added Chinese translation
2024-08-23 08:14:24 +00:00
ed
461f31582d add IDs for ricing (#93) + fix a11y bleed 2024-08-22 20:14:08 +00:00
ed
f356faa278 u2c: support multiple exclusion patterns 2024-08-22 20:03:25 +00:00
ed
9f034d9c4c fix confusing logmsg for zerobyte files 2024-08-22 19:54:10 +00:00
ed
ba52590ae4 translation tweaks 2024-08-22 19:52:20 +00:00
ultwcz
92edea1de5 add translation: Chinese (#94) 2024-08-22 17:19:16 +00:00
ed
7ff46966da fix some issues with shares mentioned in #84;
* crash when root volume is unmapped
* rephrase login-page for shares
* add chrome support (lol)
* fix confusing helptext
* improve ux
  * placeholders in share creator
  * button to disable expiration in share creator
  * human-readable timestamps in share listing
2024-08-19 21:38:47 +00:00
ed
fca70b3508 update pkgs to 1.14.1 2024-08-19 00:24:52 +00:00
ed
70009cd984 v1.14.1 2024-08-19 00:14:44 +00:00
ed
8d8b88c4fd update pkgs to 1.14.0 2024-08-18 23:36:57 +00:00
ed
c4b0cccefd v1.14.0 2024-08-18 23:11:36 +00:00
ed
7c2beba555 add file/folder sharing; closes #84 2024-08-18 22:49:13 +00:00
ed
7d8d94388b invert volume scrollwheel
<daniiooo> also iirc some time ago we were talking about the scroll for volume ed
<daniiooo> and how its reversed
<ed> is it reversed though? most people said it worked the way they expected
<daniiooo> fuck maybe i agreed back then too
<daniiooo> its the opposite in both aimp and mpv though
<ed> is it w
<tatsu> its a feature
<Devices> it's to keep you on your toes
<Devices> consciously use copyparty
<ed> i can invert it no problem
<ed> would be a nice surprise for anyone who's used it
<Flaminator> Scroll down turns the audio down right?
<daniiooo> ye it makes it louder in cpp
<Devices> why would scrolling down make something louder
<Vin> yeah that's odd
<Vin> scrolling up should make it louder
<Flaminator> It's what it does for me in winamp, mpc-hc and foobar2000.
<daniiooo> so now the question is who itc agreed to whats currently in cpp
<daniiooo> haha
<ed> idk but i'm inverting it
<ed> let's invert it every 6 months
2024-08-17 20:36:59 +00:00
ed
0b46b1a614 fix some vproxy issues (#93):
* navpane would always feed the vproxy paths into the tree
   instead of only when necessary (the initial load)

* mkdir would return `X-New-Dir` without the `rp-loc` prefix
  * chpw and some other redirects also sent raw vpaths

Reported-by: @iridial
2024-08-17 18:17:40 +00:00
ed
5153db6bff ux: login margin; theme2: yellow buttons
the red buttons from protonmail's monokai theme look better,
but they're confusing because intuitively red means off
2024-08-17 15:55:55 +00:00
ed
b0af4b3712 hook/reloc: dupe in one vol doesn't mean dupe in another 2024-08-16 21:08:22 +00:00
ed
c8f4aeaefa hook/reloc: fix up2k jank
* wark landed in the wrong registry when moved to another volume
   (harmless; upload would succeed on the next handshake)

* dedup did not apply correctly when moved into another volume,
   since all the checks were done based on the previous vol;
   fix this by recursing the whole thing

also update the reloc example after some real-world experience

Reported-by: @daniiooo
2024-08-15 19:26:06 +00:00
ed
00da74400c password-changer fixes:
* fix `--chpw-no` which did nothing
* print list of users with unchanged passwords by default
* more granular verbosity levels
2024-08-15 17:30:01 +00:00
ed
83fb569d61 make passwords user-changeable; closes #92 2024-08-14 20:09:57 +00:00
ed
5a62cb4869 fix custom fonts in sandboxed docs;
`@import` must be at the very start of a `<style>` tag

Reported-by: @thaddeuskkr (thx!)
2024-08-14 15:30:04 +00:00
ed
687df2fabd unix-socket fixes:
* support x-forwarded-for
* option to specify socket permissions and group
* in containers, avoid collision during restart
* add --help-bind with examples
2024-08-14 04:47:10 +00:00
ed
cdd0794d6e update pkgs to 1.13.8 2024-08-13 00:20:04 +00:00
71 changed files with 4741 additions and 776 deletions

126
README.md
View File

@@ -1,4 +1,6 @@
# 💾🎉 copyparty <img src="https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg" width="250" align="right"/>
### 💾🎉 copyparty
turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser
@@ -41,7 +43,9 @@ turn almost any device into a file server with resumable uploads/downloads using
* [unpost](#unpost) - undo/delete accidental uploads * [unpost](#unpost) - undo/delete accidental uploads
* [self-destruct](#self-destruct) - uploads can be given a lifetime * [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)) * [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) * [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 * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
* [media player](#media-player) - plays almost every audio format there is * [media player](#media-player) - plays almost every audio format there is
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression) * [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
@@ -62,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 * [smb server](#smb-server) - unsafe, slow, not recommended for wan
* [browser ux](#browser-ux) - tweaking the ui * [browser ux](#browser-ux) - tweaking the ui
* [opengraph](#opengraph) - discord and social-media embeds * [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 * [exclude-patterns](#exclude-patterns) - to save some time
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems * [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
* [periodic rescan](#periodic-rescan) - filesystem monitoring * [periodic rescan](#periodic-rescan) - filesystem monitoring
@@ -76,6 +81,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/)) * [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/)) * [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such * [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 * [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 dont wanna be indexed
* [themes](#themes) * [themes](#themes)
@@ -235,7 +241,7 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ ...of videos using FFmpeg * ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually) * ☑ 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) * ☑ SPA (browse while uploading)
* server indexing * server indexing
* ☑ [locate files by contents](#file-search) * ☑ [locate files by contents](#file-search)
@@ -726,6 +732,13 @@ 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 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 ## file manager
cut/paste, rename, and delete files/folders (if you have permission) cut/paste, rename, and delete files/folders (if you have permission)
@@ -744,6 +757,42 @@ file selection: click somewhere on the line (not the link itsef), then:
you can move files across browser tabs (cut in one tab, paste in another) you can move files across browser tabs (cut in one tab, paste in another)
## shares
share a file or folder by creating a temporary link
when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or alternatively:
* select a folder first to share that folder instead
* select one or more files to share only those files
this feature was made with [identity providers](#identity-providers) in mind -- configure your reverseproxy to skip the IdP's access-control for a given URL prefix and use that to safely share specific files/folders sans the usual auth checks
when creating a share, the creator can choose any of the following options:
* password-protection
* expire after a certain time; `0` or blank means infinite
* allow visitors to upload (if the user who creates the share has write-access)
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
* 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)
specify `--shr /foobar` to enable this feature; a toplevel virtual folder named `foobar` is then created, and that's where all the shares will be served from
* you can name it whatever, `foobar` is just an example
* if you're using config files, put `shr: /foobar` inside the `[global]` section instead
users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there
**security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
## batch rename ## batch rename
select some files and press `F2` to bring up the rename UI select some files and press `F2` to bring up the rename UI
@@ -1115,9 +1164,41 @@ NOTE: because discord (and maybe others) strip query args such as `?raw` in open
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) 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`
when someone tries to upload a file that already exists on the server, the upload will be politely declined and a symlink is created instead, pointing to the nearest copy on disk, thus reducinc disk space usage
**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 ## 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. 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.
@@ -1131,7 +1212,6 @@ through arguments:
* `-e2v` verfies file integrity at startup, comparing hashes from the db * `-e2v` verfies file integrity at startup, comparing hashes from the db
* `-e2vu` patches the database with the new hashes from the filesystem * `-e2vu` patches the database with the new hashes from the filesystem
* `-e2vp` panics and kills copyparty instead * `-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: 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 * `-v ~/music::r:c,e2ds,e2tsr` does a full reindex of everything on startup
@@ -1144,7 +1224,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) * 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 * `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 * 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 ### exclude-patterns
@@ -1355,6 +1434,29 @@ there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik)
a more complete example of the copyparty configuration options [look like this](./docs/examples/docker/idp/copyparty.conf) a more complete example of the copyparty configuration options [look like this](./docs/examples/docker/idp/copyparty.conf)
but if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead
## user-changeable passwords
if permitted, users can change their own passwords in the control-panel
* not compatible with [identity providers](#identity-providers)
* must be enabled with `--chpw` because account-sharing is a popular usecase
* if you want to enable the feature but deny password-changing for a specific list of accounts, you can do that with `--chpw-no name1,name2,name3,...`
* to perform a password reset, edit the server config and give the user another password there, then do a [config reload](#server-config) or server restart
* the custom passwords are kept in a textfile at filesystem-path `--chpw-db`, by default `chpw.json` in the copyparty config folder
* 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
* ...which means that all user-defined passwords will be forgotten if you change password-hashing settings
## using the cloud as storage ## using the cloud as storage
@@ -1455,11 +1557,13 @@ 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 * 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 * 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 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 * **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 * depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
for improved security (and a tiny performance boost) consider listening on a unix-socket with `-i /tmp/party.sock` instead of `-i 127.0.0.1` for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/tmp/party.sock` (permission `770` means only members of group `www` can access it)
example webserver configs: example webserver configs:
@@ -1785,6 +1889,7 @@ interact with copyparty using non-browser clients
* [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md) * [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) * 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) * contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
@@ -1861,6 +1966,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 * `-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 * `--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 * 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 * `--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` * 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) * `--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)
@@ -1900,7 +2008,7 @@ some notes on hardening
* cors doesn't work right otherwise * cors doesn't work right otherwise
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml` * if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
* this returns html documents as plaintext, and also disables markdown rendering * this returns html documents as plaintext, and also disables markdown rendering
* when running behind a reverse-proxy, listen on a unix-socket with `-i /tmp/party.sock` instead of `-i 127.0.0.1` for tighter access control (plus you get a tiny performance boost for free) * when running behind a reverse-proxy, listen on a unix-socket for tighter access control (and more performance); see [reverse-proxy](#reverse-proxy) or `--help-bind`
safety profiles: safety profiles:
@@ -1915,7 +2023,7 @@ safety profiles:
* `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance * `--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 * 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 * `--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: * option `-sss` is a shortcut for the above plus:
* `--no-dav` disables webdav support * `--no-dav` disables webdav support

View File

@@ -2,11 +2,12 @@
import json import json
import os import os
import re
import sys import sys
_ = r""" _ = r"""
relocate/redirect incoming uploads according to file extension relocate/redirect incoming uploads according to file extension or name
example usage as global config: example usage as global config:
--xbu j,c1,bin/hooks/reloc-by-ext.py --xbu j,c1,bin/hooks/reloc-by-ext.py
@@ -52,11 +53,19 @@ def main():
try: try:
fn, ext = fn.rsplit(".", 1) fn, ext = fn.rsplit(".", 1)
except: except:
# no file extension; abort # no file extension; pretend it's "bin"
return ext = "bin"
ext = ext.lower() ext = ext.lower()
# this function must end by printing the action to perform;
# that's handled by the print(json.dumps(... at the bottom
#
# the action can contain the following keys:
# "vp" is the folder URL to move the upload to,
# "ap" is the filesystem-path to move it to (but "vp" is safer),
# "fn" overrides the final filename to use
## ##
## some example actions to take; pick one by ## some example actions to take; pick one by
## selecting it inside the print at the end: ## selecting it inside the print at the end:
@@ -80,11 +89,37 @@ def main():
elif ext in MUSIC.split(): elif ext in MUSIC.split():
by_category = {"vp": "/just/tunes"} by_category = {"vp": "/just/tunes"}
else: else:
by_category = {} by_category = {} # no action
# now choose the effect to apply; can be any of these: # now choose the default effect to apply; can be any of these:
# into_subfolder into_toplevel into_sibling by_category # into_subfolder into_toplevel into_sibling by_category
effect = into_subfolder effect = {"vp": "/junk"}
##
## but we can keep going, adding more speicifc rules
## which can take precedence, replacing the fallback
## effect we just specified:
##
fn = fn.lower() # lowercase filename to make this easier
if "screenshot" in fn:
effect = {"vp": "/ss"}
if "mpv_" in fn:
effect = {"vp": "/anishots"}
elif "debian" in fn or "biebian" in fn:
effect = {"vp": "/linux-ISOs"}
elif re.search(r"ep(isode |\.)?[0-9]", fn):
effect = {"vp": "/podcasts"}
# regex lets you grab a part of the matching
# text and use that in the upload path:
m = re.search(r"\b(op|ed)([^a-z]|$)", fn)
if m:
# the regex matched; use "anime-op" or "anime-ed"
effect = {"vp": "/anime-" + m[1]}
# aaand DO IT
print(json.dumps({"reloc": effect})) print(json.dumps({"reloc": effect}))

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
S_VERSION = "1.22" S_VERSION = "1.24"
S_BUILD_DT = "2024-08-08" S_BUILD_DT = "2024-09-05"
""" """
u2c.py: upload to copyparty u2c.py: upload to copyparty
@@ -41,18 +41,24 @@ except:
try: try:
import requests import requests
req_ses = requests.Session()
except ImportError as ex: except ImportError as ex:
if EXE: if "-" in sys.argv or "-h" in sys.argv:
m = ""
elif EXE:
raise raise
elif sys.version_info > (2, 7): elif sys.version_info > (2, 7):
m = "\nERROR: need 'requests'; please run this command:\n {0} -m pip install --user requests\n" m = "\nERROR: need 'requests'{0}; please run this command:\n {1} -m pip install --user requests\n"
else: else:
m = "requests/2.18.4 urllib3/1.23 chardet/3.0.4 certifi/2020.4.5.1 idna/2.7" 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 = [" https://pypi.org/project/" + x + "/#files" for x in m.split()]
m = "\n ERROR: need these:\n" + "\n".join(m) + "\n" m = "\n ERROR: need these{0}:\n" + "\n".join(m) + "\n"
m += "\n for f in *.whl; do unzip $f; done; rm -r *.dist-info\n" m += "\n for f in *.whl; do unzip $f; done; rm -r *.dist-info\n"
print(m.format(sys.executable), "\nspecifically,", ex) if m:
t = " when not running with '-h' or url '-'"
print(m.format(t, sys.executable), "\nspecifically,", ex)
sys.exit(1) sys.exit(1)
@@ -76,7 +82,22 @@ else:
VT100 = platform.system() != "Windows" 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()
class Daemon(threading.Thread): class Daemon(threading.Thread):
@@ -271,6 +292,12 @@ class MTHash(object):
_print = print _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): def eprint(*a, **ka):
ka["file"] = sys.stderr ka["file"] = sys.stderr
ka["end"] = "" ka["end"] = ""
@@ -284,18 +311,17 @@ def eprint(*a, **ka):
def flushing_print(*a, **ka): def flushing_print(*a, **ka):
try: try:
_print(*a, **ka) safe_print(*a, **ka)
except: except:
v = " ".join(str(x) for x in a) v = " ".join(str(x) for x in a)
v = v.encode("ascii", "replace").decode("ascii") v = v.encode("ascii", "replace").decode("ascii")
_print(v, **ka) safe_print(v, **ka)
if "flush" not in ka: if "flush" not in ka:
sys.stdout.flush() sys.stdout.flush()
if not VT100: print = safe_print if VT100 else flushing_print
print = flushing_print
def termsize(): def termsize():
@@ -770,8 +796,6 @@ class Ctl(object):
self.up_c = 0 self.up_c = 0
self.up_b = 0 self.up_b = 0
self.up_br = 0 self.up_br = 0
self.hasher_busy = 1
self.handshaker_busy = 0
self.uploader_busy = 0 self.uploader_busy = 0
self.serialized = False self.serialized = False
@@ -781,6 +805,9 @@ class Ctl(object):
self.eta = "99:99:99" self.eta = "99:99:99"
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.exit_cond = threading.Condition()
self.uploader_alive = ar.j
self.handshaker_alive = ar.j
self.q_handshake = Queue() # type: Queue[File] self.q_handshake = Queue() # type: Queue[File]
self.q_upload = Queue() # type: Queue[FileSlice] self.q_upload = Queue() # type: Queue[FileSlice]
@@ -851,27 +878,21 @@ class Ctl(object):
Daemon(self.handshaker) Daemon(self.handshaker)
Daemon(self.uploader) Daemon(self.uploader)
idles = 0 while True:
while idles < 3: with self.exit_cond:
time.sleep(0.07) self.exit_cond.wait(0.07)
with self.mutex: with self.mutex:
if ( if not self.handshaker_alive and not self.uploader_alive:
self.q_handshake.empty() break
and self.q_upload.empty() st_hash = self.st_hash[:]
and not self.hasher_busy st_up = self.st_up[:]
and not self.handshaker_busy
and not self.uploader_busy
):
idles += 1
else:
idles = 0
if VT100 and not self.ar.ns: if VT100 and not self.ar.ns:
maxlen = ss.w - len(str(self.nfiles)) - 14 maxlen = ss.w - len(str(self.nfiles)) - 14
txt = "\033[s\033[{0}H".format(ss.g) txt = "\033[s\033[{0}H".format(ss.g)
for y, k, st, f in [ for y, k, st, f in [
[0, "hash", self.st_hash, self.hash_f], [0, "hash", st_hash, self.hash_f],
[1, "send", self.st_up, self.up_f], [1, "send", st_up, self.up_f],
]: ]:
txt += "\033[{0}H{1}:".format(ss.g + y, k) txt += "\033[{0}H{1}:".format(ss.g + y, k)
file, arg = st file, arg = st
@@ -1027,11 +1048,42 @@ class Ctl(object):
self.hash_f += 1 self.hash_f += 1
self.hash_c += len(file.cids) self.hash_c += len(file.cids)
self.hash_b += file.size 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:
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 = base64.urlsafe_b64encode(zb).decode("utf-8")
vp = file.rel.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.q_handshake.put(file)
self.hasher_busy = 0
self.st_hash = [None, "(finished)"] 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): def handshaker(self):
search = self.ar.s search = self.ar.s
@@ -1039,8 +1091,10 @@ class Ctl(object):
while True: while True:
file = self.q_handshake.get() file = self.q_handshake.get()
if not file: if not file:
with self.mutex:
self.handshaker_alive -= 1
self.q_upload.put(None) self.q_upload.put(None)
break return
upath = file.abs.decode("utf-8", "replace") upath = file.abs.decode("utf-8", "replace")
if not VT100: if not VT100:
@@ -1052,9 +1106,6 @@ class Ctl(object):
self.errs += 1 self.errs += 1
continue continue
with self.mutex:
self.handshaker_busy += 1
while time.time() < file.cd: while time.time() < file.cd:
time.sleep(0.1) time.sleep(0.1)
@@ -1062,17 +1113,17 @@ class Ctl(object):
if search: if search:
if hs: if hs:
for hit in hs: for hit in hs:
t = "found: {0}\n {1}{2}\n" t = "found: {0}\n {1}{2}"
print(t.format(upath, burl, hit["rp"]), end="") print(t.format(upath, burl, hit["rp"]))
else: else:
print("NOT found: {0}\n".format(upath), end="") print("NOT found: {0}".format(upath))
with self.mutex: with self.mutex:
self.up_f += 1 self.up_f += 1
self.up_c += len(file.cids) self.up_c += len(file.cids)
self.up_b += file.size self.up_b += file.size
self.handshaker_busy -= 1
self._check_if_done()
continue continue
if file.recheck: if file.recheck:
@@ -1104,7 +1155,6 @@ class Ctl(object):
file.up_b -= sz file.up_b -= sz
file.ucids = hs file.ucids = hs
self.handshaker_busy -= 1
if not hs: if not hs:
self.at_hash += file.t_hash self.at_hash += file.t_hash
@@ -1130,6 +1180,9 @@ class Ctl(object):
kw = "uploaded" if file.up_b else " found" kw = "uploaded" if file.up_b else " found"
print("{0} {1}".format(kw, upath)) print("{0} {1}".format(kw, upath))
self._check_if_done()
continue
chunksz = up2k_chunksize(file.size) chunksz = up2k_chunksize(file.size)
njoin = (self.ar.sz * 1024 * 1024) // chunksz njoin = (self.ar.sz * 1024 * 1024) // chunksz
cs = hs[:] cs = hs[:]
@@ -1149,8 +1202,16 @@ class Ctl(object):
while True: while True:
fsl = self.q_upload.get() fsl = self.q_upload.get()
if not fsl: if not fsl:
done = False
with self.mutex:
self.uploader_alive -= 1
if not self.uploader_alive:
done = not self.handshaker_alive
self.st_up = [None, "(finished)"] self.st_up = [None, "(finished)"]
break if done:
with self.exit_cond:
self.exit_cond.notify_all()
return
file = fsl.file file = fsl.file
cids = fsl.cids cids = fsl.cids
@@ -1236,7 +1297,7 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("-v", action="store_true", help="verbose") ap.add_argument("-v", action="store_true", help="verbose")
ap.add_argument("-a", metavar="PASSWD", help="password or $filepath") ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)") ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\\.hist/.*'") ap.add_argument("-x", type=unicode, metavar="REGEX", action="append", help="skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\.hist/.*'")
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible") ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)") ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming") ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
@@ -1252,6 +1313,10 @@ 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("--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.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("--jw", action="store_true", help="just identifier+filepath, not mtime/size too")
ap = app.add_argument_group("performance tweaks") 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="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") 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")
@@ -1283,7 +1348,11 @@ source file/folder selection uses rsync syntax, meaning that:
if ar.dr: if ar.dr:
ar.ow = True ar.ow = True
for k in "dl dr drd".split(): ar.x = "|".join(ar.x or [])
setattr(ar, "wlist", ar.url == "-")
for k in "dl dr drd wlist".split():
errs = [] errs = []
if ar.safe and getattr(ar, k): if ar.safe and getattr(ar, k):
errs.append(k) errs.append(k)

View File

@@ -19,6 +19,9 @@
* the `act:bput` thing is optional since copyparty v1.9.29 * 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) * 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) ### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
* browser integration, kind of? custom rightclick actions and stuff * browser integration, kind of? custom rightclick actions and stuff
* rightclick a pic and send it to copyparty straight from your browser * 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, # look for "max clients:" when starting copyparty, as nginx should
# the following arguments are recommended: # not accept more consecutive clients than what copyparty is able to;
#
# -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),
# nginx default is 512 (worker_processes 1, worker_connections 512) # nginx default is 512 (worker_processes 1, worker_connections 512)
# #
# you may also consider adding -j0 for CPU-intensive configurations # rarely, in some extreme usecases, it can be good to add -j0
# (5'000 requests per second, or 20gbps upload/download in parallel) # (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 # 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 # 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; server 127.0.0.1:3923 fail_timeout=1s;
keepalive 1; 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 { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
@@ -34,7 +53,8 @@ server {
#include /etc/nginx/cloudflare-only.conf; #include /etc/nginx/cloudflare-only.conf;
location / { location / {
proxy_pass http://cpp; # recommendation: replace cpp_tcp with cpp_uds below
proxy_pass http://cpp_tcp;
proxy_redirect off; proxy_redirect off;
# disable buffering (next 4 lines) # disable buffering (next 4 lines)
proxy_http_version 1.1; proxy_http_version 1.1;
@@ -52,6 +72,7 @@ server {
} }
} }
# default client_max_body_size (1M) blocks uploads larger than 256 MiB # default client_max_body_size (1M) blocks uploads larger than 256 MiB
client_max_body_size 1024M; client_max_body_size 1024M;
client_header_timeout 610m; client_header_timeout 610m;

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty pkgname=copyparty
pkgver="1.13.6" pkgver="1.15.1"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") 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") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" ) backup=("etc/${pkgname}.d/init" )
sha256sums=("abb9d0df24c4082594adc2070b7e9fb84a57733d0a3fbf83f4ec2a02813dd717") sha256sums=("5fb048fe7e2aa5ad18c9cdb333af3ee5e51c338efa74b34aa8aa444675eac913")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}" cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.13.6/copyparty-sfx.py", "url": "https://github.com/9001/copyparty/releases/download/v1.15.1/copyparty-sfx.py",
"version": "1.13.6", "version": "1.15.1",
"hash": "sha256-eQSHZYXIbS4piCU/zOyEz3E6V/lS6348vENMLwONYUM=" "hash": "sha256-i4S/TmuAphv/wbndfoSUYztNqO+o+qh/v8GcslxWWUk="
} }

View File

@@ -1,8 +1,8 @@
(function() { (function() {
// usage: copy this to '.banner.js' in your webroot, // usage: copy this to '.banner.js' in your webroot,
// and run copyparty with the following argument: // and run copyparty with the following arguments:
// --body-foot '<script src="/.banner.js"></script>' // --js-browser /.banner.js --js-other /.banner.js

View File

@@ -40,8 +40,6 @@ html.ey {
--u2-b1-bg: var(--w2); --u2-b1-bg: var(--w2);
--u2-b2-bg: var(--w2); --u2-b2-bg: var(--w2);
--u2-o-bg: var(--w2);
--u2-o-1-bg: var(--a);
--u2-txt-bg: var(--w2); --u2-txt-bg: var(--w2);
--u2-tab-bg: a; --u2-tab-bg: a;
--u2-tab-1-bg: var(--w2); --u2-tab-1-bg: var(--w2);

View File

@@ -19,6 +19,7 @@ if True:
from typing import Any, Callable from typing import Any, Callable
PY2 = sys.version_info < (3,) PY2 = sys.version_info < (3,)
PY36 = sys.version_info > (3, 6)
if not PY2: if not PY2:
unicode: Callable[[Any], str] = str unicode: Callable[[Any], str] = str
else: else:

View File

@@ -27,6 +27,7 @@ from .__init__ import (
EXE, EXE,
MACOS, MACOS,
PY2, PY2,
PY36,
VT100, VT100,
WINDOWS, WINDOWS,
E, E,
@@ -54,6 +55,7 @@ from .util import (
Daemon, Daemon,
align_tab, align_tab,
ansi_re, ansi_re,
b64enc,
dedent, dedent,
min_ex, min_ex,
pybin, pybin,
@@ -204,7 +206,7 @@ def init_E(EE: EnvParams) -> None:
errs.append("Using [%s] instead" % (p,)) errs.append("Using [%s] instead" % (p,))
if errs: if errs:
print("WARNING: " + ". ".join(errs)) warn(". ".join(errs))
return p # type: ignore return p # type: ignore
except Exception as ex: except Exception as ex:
@@ -234,7 +236,7 @@ def init_E(EE: EnvParams) -> None:
raise raise
def get_srvname() -> str: def get_srvname(verbose) -> str:
try: try:
ret: str = unicode(socket.gethostname()).split(".")[0] ret: str = unicode(socket.gethostname()).split(".")[0]
except: except:
@@ -244,6 +246,7 @@ def get_srvname() -> str:
return ret return ret
fp = os.path.join(E.cfg, "name.txt") fp = os.path.join(E.cfg, "name.txt")
if verbose:
lprint("using hostname from {}\n".format(fp)) lprint("using hostname from {}\n".format(fp))
try: try:
with open(fp, "rb") as f: with open(fp, "rb") as f:
@@ -266,7 +269,7 @@ def get_fk_salt() -> str:
with open(fp, "rb") as f: with open(fp, "rb") as f:
ret = f.read().strip() ret = f.read().strip()
except: except:
ret = base64.b64encode(os.urandom(18)) ret = b64enc(os.urandom(18))
with open(fp, "wb") as f: with open(fp, "wb") as f:
f.write(ret + b"\n") f.write(ret + b"\n")
@@ -279,7 +282,7 @@ def get_dk_salt() -> str:
with open(fp, "rb") as f: with open(fp, "rb") as f:
ret = f.read().strip() ret = f.read().strip()
except: except:
ret = base64.b64encode(os.urandom(30)) ret = b64enc(os.urandom(30))
with open(fp, "wb") as f: with open(fp, "wb") as f:
f.write(ret + b"\n") f.write(ret + b"\n")
@@ -292,7 +295,7 @@ def get_ah_salt() -> str:
with open(fp, "rb") as f: with open(fp, "rb") as f:
ret = f.read().strip() ret = f.read().strip()
except: except:
ret = base64.b64encode(os.urandom(18)) ret = b64enc(os.urandom(18))
with open(fp, "wb") as f: with open(fp, "wb") as f:
f.write(ret + b"\n") f.write(ret + b"\n")
@@ -350,7 +353,7 @@ def configure_ssl_ver(al: argparse.Namespace) -> None:
# oh man i love openssl # oh man i love openssl
# check this out # check this out
# hold my beer # hold my beer
assert ssl # type: ignore assert ssl # type: ignore # !rm
ptn = re.compile(r"^OP_NO_(TLS|SSL)v") ptn = re.compile(r"^OP_NO_(TLS|SSL)v")
sslver = terse_sslver(al.ssl_ver).split(",") sslver = terse_sslver(al.ssl_ver).split(",")
flags = [k for k in ssl.__dict__ if ptn.match(k)] flags = [k for k in ssl.__dict__ if ptn.match(k)]
@@ -384,7 +387,7 @@ def configure_ssl_ver(al: argparse.Namespace) -> None:
def configure_ssl_ciphers(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) ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
if al.ssl_ver: if al.ssl_ver:
ctx.options &= ~al.ssl_flags_en ctx.options &= ~al.ssl_flags_en
@@ -527,6 +530,41 @@ def showlic() -> None:
def get_sects(): def get_sects():
return [ return [
[
"bind",
"configure listening",
dedent(
"""
\033[33m-i\033[0m takes a comma-separated list of interfaces to listen on;
IP-addresses and/or unix-sockets (Unix Domain Sockets)
the default (\033[32m-i ::\033[0m) means all IPv4 and IPv6 addresses
\033[32m-i 0.0.0.0\033[0m listens on all IPv4 NICs/subnets
\033[32m-i 127.0.0.1\033[0m listens on IPv4 localhost only
\033[32m-i 127.1\033[0m listens on IPv4 localhost only
\033[32m-i 127.1,192.168.123.1\033[0m = IPv4 localhost and 192.168.123.1
\033[33m-p\033[0m takes a comma-separated list of tcp ports to listen on;
the default is \033[32m-p 3923\033[0m but as root you can \033[32m-p 80,443,3923\033[0m
when running behind a reverse-proxy, it's recommended to
use unix-sockets for improved performance and security;
\033[32m-i unix:770:www:\033[33m/tmp/a.sock\033[0m listens on \033[33m/tmp/a.sock\033[0m with
permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m
group. This is the best approach. Alternatively,
\033[32m-i unix:777:\033[33m/tmp/a.sock\033[0m sets perms \033[33m0777\033[0m so anyone can
access it; bad unless it's inside a restricted folder
\033[32m-i unix:\033[33m/tmp/a.sock\033[0m keeps umask-defined permissions
(usually \033[33m0600\033[0m) and the same user/group as copyparty
\033[33m-p\033[0m (tcp ports) is ignored for unix sockets
"""
),
],
[ [
"accounts", "accounts",
"accounts and volumes", "accounts and volumes",
@@ -937,6 +975,16 @@ def add_fs(ap):
ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media") ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media")
def add_share(ap):
db_path = os.path.join(E.cfg, "shares.db")
ap2 = ap.add_argument_group('share-url options')
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
ap2.add_argument("--shr-v", action="store_true", help="debug")
def add_upload(ap): def add_upload(ap):
ap2 = ap.add_argument_group('upload options') ap2 = ap.add_argument_group('upload options')
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m") ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
@@ -947,9 +995,10 @@ 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("--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("--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("--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("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
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("--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("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead (volflag=copydupes)") 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-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
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("--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-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")
@@ -969,8 +1018,8 @@ def add_upload(ap):
def add_network(ap): def add_network(ap):
ap2 = ap.add_argument_group('network options') ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=u, default="::", help="ip to bind (comma-sep.) and/or [\033[32munix:/tmp/a.sock\033[0m], default: all IPv4 and IPv6") ap2.add_argument("-i", metavar="IP", type=u, default="::", help="IPs and/or unix-sockets to listen on (see \033[33m--help-bind\033[0m). Default: all IPv4 and IPv6")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range); ignored for unix-sockets") ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to listen on (comma/range); ignored for unix-sockets")
ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)") ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy")
ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from") ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from")
@@ -1021,6 +1070,7 @@ def add_cert(ap, cert_path):
def add_auth(ap): 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 = 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-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") 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")
@@ -1028,6 +1078,19 @@ 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("--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("--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("--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")
def add_chpw(ap):
db_path = os.path.join(E.cfg, "chpw.json")
ap2 = ap.add_argument_group('user-changeable passwords options')
ap2.add_argument("--chpw", action="store_true", help="allow users to change their own passwords")
ap2.add_argument("--chpw-no", metavar="U,U,U", type=u, action="append", help="do not allow password-changes for this comma-separated list of usernames")
ap2.add_argument("--chpw-db", metavar="PATH", type=u, default=db_path, help="where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--chpw-len", metavar="N", type=int, default=8, help="minimum password length")
ap2.add_argument("--chpw-v", metavar="LVL", type=int, default=2, help="verbosity of summary on config load [\033[32m0\033[0m] = nothing at all, [\033[32m1\033[0m] = number of users, [\033[32m2\033[0m] = list users with default-pw, [\033[32m3\033[0m] = list all users")
def add_zeroconf(ap): def add_zeroconf(ap):
@@ -1170,6 +1233,7 @@ def add_optouts(ap):
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar") 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-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-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-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") ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
@@ -1230,6 +1294,7 @@ def add_logging(ap):
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") 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("--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-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-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") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
@@ -1288,7 +1353,7 @@ def add_transcoding(ap):
def add_db_general(ap, hcores): def add_db_general(ap, hcores):
noidx = APPLESAN_TXT if MACOS else "" noidx = APPLESAN_TXT if MACOS else ""
ap2 = ap.add_argument_group('general db options') 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("-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("-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") ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db")
@@ -1297,11 +1362,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("--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-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-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("--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("--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("--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("--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("--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("--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, ...)") 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, ...)")
@@ -1358,7 +1425,7 @@ def add_ui(ap, retry):
ap2 = ap.add_argument_group('ui options') ap2 = ap.add_argument_group('ui options')
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)") ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed") ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
@@ -1410,10 +1477,11 @@ def add_debug(ap):
def run_argparse( 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: ) -> argparse.Namespace:
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
formatter_class=formatter, formatter_class=formatter,
usage=argparse.SUPPRESS,
prog="copyparty", prog="copyparty",
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT), description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
) )
@@ -1431,18 +1499,20 @@ def run_argparse(
tty = os.environ.get("TERM", "").lower() == "linux" tty = os.environ.get("TERM", "").lower() == "linux"
srvname = get_srvname() srvname = get_srvname(verbose)
add_general(ap, nc, srvname) add_general(ap, nc, srvname)
add_network(ap) add_network(ap)
add_tls(ap, cert_path) add_tls(ap, cert_path)
add_cert(ap, cert_path) add_cert(ap, cert_path)
add_auth(ap) add_auth(ap)
add_chpw(ap)
add_qr(ap, tty) add_qr(ap, tty)
add_zeroconf(ap) add_zeroconf(ap)
add_zc_mdns(ap) add_zc_mdns(ap)
add_zc_ssdp(ap) add_zc_ssdp(ap)
add_fs(ap) add_fs(ap)
add_share(ap)
add_upload(ap) add_upload(ap)
add_db_general(ap, hcores) add_db_general(ap, hcores)
add_db_metadata(ap) add_db_metadata(ap)
@@ -1562,6 +1632,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
("--hdr-au-usr", "--idp-h-usr"), ("--hdr-au-usr", "--idp-h-usr"),
("--idp-h-sep", "--idp-gsep"), ("--idp-h-sep", "--idp-gsep"),
("--th-no-crop", "--th-crop=n"), ("--th-no-crop", "--th-crop=n"),
("--never-symlink", "--hardlink-only"),
] ]
for dk, nk in deprecated: for dk, nk in deprecated:
idx = -1 idx = -1
@@ -1586,7 +1657,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
argv.extend(["--qr"]) argv.extend(["--qr"])
if ANYWIN or not os.geteuid(): if ANYWIN or not os.geteuid():
# win10 allows symlinks if admin; can be unexpected # 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: except:
pass pass
@@ -1608,7 +1679,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]: for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
try: try:
al = run_argparse(argv, fmtr, retry, nc) al = run_argparse(argv, fmtr, retry, nc)
dal = run_argparse([], fmtr, retry, nc) dal = run_argparse([], fmtr, retry, nc, False)
break break
except SystemExit: except SystemExit:
raise raise
@@ -1692,7 +1763,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
print("error: python2 cannot --smb") print("error: python2 cannot --smb")
return return
if sys.version_info < (3, 6): if not PY36:
al.no_scandir = True al.no_scandir = True
if not hasattr(os, "sendfile"): if not hasattr(os, "sendfile"):

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 13, 8) VERSION = (1, 15, 2)
CODENAME = "race the beam" CODENAME = "fill the drives"
BUILD_DT = (2024, 8, 13) BUILD_DT = (2024, 9, 16)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
import argparse import argparse
import base64 import base64
import hashlib import hashlib
import json
import os import os
import re import re
import stat import stat
@@ -34,9 +35,11 @@ from .util import (
odfusion, odfusion,
relchk, relchk,
statdir, statdir,
ub64enc,
uncyg, uncyg,
undot, undot,
unhumanize, unhumanize,
vjoin,
vsplit, vsplit,
) )
@@ -341,6 +344,8 @@ class VFS(object):
self.histtab: dict[str, str] = {} # all realpath->histpath self.histtab: dict[str, str] = {} # all realpath->histpath
self.dbv: Optional[VFS] = None # closest full/non-jump parent self.dbv: Optional[VFS] = None # closest full/non-jump parent
self.lim: Optional[Lim] = None # upload limits; only set for dbv self.lim: Optional[Lim] = None # upload limits; only set for dbv
self.shr_src: Optional[tuple[VFS, str]] = None # source vfs+rem of a share
self.shr_files: set[str] = set() # filenames to include from shr_src
self.aread: dict[str, list[str]] = {} self.aread: dict[str, list[str]] = {}
self.awrite: dict[str, list[str]] = {} self.awrite: dict[str, list[str]] = {}
self.amove: dict[str, list[str]] = {} self.amove: dict[str, list[str]] = {}
@@ -365,6 +370,9 @@ class VFS(object):
self.all_aps = [] self.all_aps = []
self.all_vps = [] self.all_vps = []
self.get_dbv = self._get_dbv
self.ls = self._ls
def __repr__(self) -> str: def __repr__(self) -> str:
return "VFS(%s)" % ( return "VFS(%s)" % (
", ".join( ", ".join(
@@ -526,7 +534,15 @@ class VFS(object):
return vn, rem return vn, rem
def get_dbv(self, vrem: str) -> tuple["VFS", str]: def _get_share_src(self, vrem: str) -> tuple["VFS", str]:
src = self.shr_src
if not src:
return self._get_dbv(vrem)
shv, srem = src
return shv, vjoin(srem, vrem)
def _get_dbv(self, vrem: str) -> tuple["VFS", str]:
dbv = self.dbv dbv = self.dbv
if not dbv: if not dbv:
return self, vrem return self, vrem
@@ -552,7 +568,26 @@ class VFS(object):
ad, fn = os.path.split(ap) ad, fn = os.path.split(ap)
return os.path.join(absreal(ad), fn) return os.path.join(absreal(ad), fn)
def ls( def _ls_nope(
self, *a, **ka
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
raise Pebkac(500, "nope.avi")
def _ls_shr(
self,
rem: str,
uname: str,
scandir: bool,
permsets: list[list[bool]],
lstat: bool = False,
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
"""replaces _ls for certain shares (single-file, or file selection)"""
vn, rem = self.shr_src # type: ignore
abspath, real, _ = vn.ls(rem, "\n", scandir, permsets, lstat)
real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
return abspath, real, {}
def _ls(
self, self,
rem: str, rem: str,
uname: str, uname: str,
@@ -805,8 +840,11 @@ class AuthSrv(object):
# fwd-decl # fwd-decl
self.vfs = VFS(log_func, "", "", AXS(), {}) self.vfs = VFS(log_func, "", "", AXS(), {})
self.acct: dict[str, str] = {} self.acct: dict[str, str] = {} # uname->pw
self.iacct: dict[str, str] = {} 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.grps: dict[str, list[str]] = {}
self.re_pwd: Optional[re.Pattern] = None self.re_pwd: Optional[re.Pattern] = None
@@ -817,6 +855,7 @@ class AuthSrv(object):
self.idp_accs: dict[str, list[str]] = {} # username->groupnames self.idp_accs: dict[str, list[str]] = {} # username->groupnames
self.idp_usr_gh: dict[str, str] = {} # username->group-header-value (cache) self.idp_usr_gh: dict[str, str] = {} # username->group-header-value (cache)
self.hid_cache: dict[str, str] = {}
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.reload() self.reload()
@@ -1352,7 +1391,7 @@ class AuthSrv(object):
flags[name] = vals flags[name] = vals
self._e("volflag [{}] += {} ({})".format(name, vals, desc)) self._e("volflag [{}] += {} ({})".format(name, vals, desc))
def reload(self) -> None: def reload(self, verbosity: int = 9) -> None:
""" """
construct a flat list of mountpoints and usernames construct a flat list of mountpoints and usernames
first from the commandline arguments first from the commandline arguments
@@ -1360,9 +1399,9 @@ class AuthSrv(object):
before finally building the VFS before finally building the VFS
""" """
with self.mutex: with self.mutex:
self._reload() self._reload(verbosity)
def _reload(self) -> None: def _reload(self, verbosity: int = 9) -> None:
acct: dict[str, str] = {} # username:password acct: dict[str, str] = {} # username:password
grps: dict[str, list[str]] = {} # groupname:usernames grps: dict[str, list[str]] = {} # groupname:usernames
daxs: dict[str, AXS] = {} daxs: dict[str, AXS] = {}
@@ -1440,6 +1479,8 @@ class AuthSrv(object):
raise raise
self.setup_pwhash(acct) self.setup_pwhash(acct)
defpw = acct.copy()
self.setup_chpw(acct)
# case-insensitive; normalize # case-insensitive; normalize
if WINDOWS: if WINDOWS:
@@ -1455,9 +1496,8 @@ class AuthSrv(object):
vfs = VFS(self.log_func, absreal("."), "", axs, {}) vfs = VFS(self.log_func, absreal("."), "", axs, {})
elif "" not in mount: elif "" not in mount:
# there's volumes but no root; make root inaccessible # there's volumes but no root; make root inaccessible
vfs = VFS(self.log_func, "", "", AXS(), {}) zsd = {"d2d": True, "tcolor": self.args.tcolor}
vfs.flags["tcolor"] = self.args.tcolor vfs = VFS(self.log_func, "", "", AXS(), zsd)
vfs.flags["d2d"] = True
maxdepth = 0 maxdepth = 0
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))): for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
@@ -1486,6 +1526,56 @@ class AuthSrv(object):
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True) vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs vol.root = vfs
enshare = self.args.shr
shr = enshare[1:-1]
shrs = enshare[1:]
if enshare:
import sqlite3
shv = VFS(self.log_func, "", shr, AXS(), {})
db_path = self.args.shr_db
db = sqlite3.connect(db_path)
cur = db.cursor()
cur2 = db.cursor()
now = time.time()
for row in cur.execute("select * from sh"):
s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row
if s_t1 and s_t1 < now:
continue
if self.args.shr_v:
t = "loading %s share [%s] by [%s] => [%s]"
self.log(t % (s_pr, s_k, s_un, s_vp))
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
zb = hashlib.sha512(s_pw.encode("utf-8")).digest()
sun = "s_%s" % (ub64enc(zb)[4:16].decode("ascii"),)
acct[sun] = s_pw
else:
sun = "*"
s_axs = AXS(
[sun] if "r" in s_pr else [],
[sun] if "w" in s_pr else [],
[sun] if "m" in s_pr else [],
[sun] if "d" in s_pr else [],
)
# don't know the abspath yet + wanna ensure the user
# still has the privs they granted, so nullmap it
shv.nodes[s_k] = VFS(
self.log_func, "", "%s/%s" % (shr, s_k), s_axs, shv.flags.copy()
)
vfs.nodes[shr] = vfs.all_vols[shr] = shv
for vol in shv.nodes.values():
vfs.all_vols[vol.vpath] = vol
vol.get_dbv = vol._get_share_src
vol.ls = vol._ls_nope
zss = set(acct) zss = set(acct)
zss.update(self.idp_accs) zss.update(self.idp_accs)
zss.discard("*") zss.discard("*")
@@ -1504,7 +1594,7 @@ class AuthSrv(object):
for usr in unames: for usr in unames:
for vp, vol in vfs.all_vols.items(): for vp, vol in vfs.all_vols.items():
zx = getattr(vol.axs, axs_key) zx = getattr(vol.axs, axs_key)
if usr in zx: if usr in zx and (not enshare or not vp.startswith(shrs)):
umap[usr].append(vp) umap[usr].append(vp)
umap[usr].sort() umap[usr].sort()
setattr(vfs, "a" + perm, umap) setattr(vfs, "a" + perm, umap)
@@ -1554,6 +1644,8 @@ class AuthSrv(object):
for usr in acct: for usr in acct:
if usr not in associated_users: if usr not in associated_users:
if enshare and usr.startswith("s_"):
continue
if len(vfs.all_vols) > 1: if len(vfs.all_vols) > 1:
# user probably familiar enough that the verbose message is not necessary # user probably familiar enough that the verbose message is not necessary
t = "account [%s] is not mentioned in any volume definitions; see --help-accounts" t = "account [%s] is not mentioned in any volume definitions; see --help-accounts"
@@ -1565,8 +1657,12 @@ class AuthSrv(object):
promote = [] promote = []
demote = [] demote = []
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
hid = self.hid_cache.get(vol.realpath)
if not hid:
zb = hashlib.sha512(afsenc(vol.realpath)).digest() zb = hashlib.sha512(afsenc(vol.realpath)).digest()
hid = base64.b32encode(zb).decode("ascii").lower() hid = base64.b32encode(zb).decode("ascii").lower()
self.hid_cache[vol.realpath] = hid
vflag = vol.flags.get("hist") vflag = vol.flags.get("hist")
if vflag == "-": if vflag == "-":
pass pass
@@ -1802,6 +1898,11 @@ class AuthSrv(object):
if len(zs) == 3: # fc5 => ffcc55 if len(zs) == 3: # fc5 => ffcc55
vol.flags["tcolor"] = "".join([x * 2 for x in zs]) 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: for k1, k2 in IMPLICATIONS:
if k1 in vol.flags: if k1 in vol.flags:
vol.flags[k2] = True vol.flags[k2] = True
@@ -1906,9 +2007,6 @@ class AuthSrv(object):
for x in drop: for x in drop:
vol.flags.pop(x) 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 # verify tags mentioned by -mt[mp] are used by -mte
local_mtp = {} local_mtp = {}
local_only_mtp = {} local_only_mtp = {}
@@ -1987,11 +2085,16 @@ class AuthSrv(object):
have_e2d = False have_e2d = False
have_e2t = False have_e2t = False
have_dedup = False
unsafe_dedup = []
t = "volumes and permissions:\n" t = "volumes and permissions:\n"
for zv in vfs.all_vols.values(): for zv in vfs.all_vols.values():
if not self.warn_anonwrite: if not self.warn_anonwrite or verbosity < 5:
break break
if enshare and (zv.vpath == shr or zv.vpath.startswith(shrs)):
continue
t += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(zv.vpath, zv.realpath) t += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(zv.vpath, zv.realpath)
for txt, attr in [ for txt, attr in [
[" read", "uread"], [" read", "uread"],
@@ -2016,9 +2119,14 @@ class AuthSrv(object):
if "e2t" in zv.flags: if "e2t" in zv.flags:
have_e2t = True 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" t += "\n"
if self.warn_anonwrite: if self.warn_anonwrite and verbosity > 4:
if not self.args.no_voldump: if not self.args.no_voldump:
self.log(t) self.log(t)
@@ -2028,10 +2136,17 @@ class AuthSrv(object):
self.log("\n\033[{}\033[0m\n".format(t)) self.log("\n\033[{}\033[0m\n".format(t))
if not have_e2t: 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) self.log(t, 6)
else: 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) self.log(t, 6)
zv, _ = vfs.get("/", "*", False, False) zv, _ = vfs.get("/", "*", False, False)
@@ -2042,7 +2157,7 @@ class AuthSrv(object):
try: try:
zv, _ = vfs.get("", "*", False, True, err=999) zv, _ = vfs.get("", "*", False, True, err=999)
if self.warn_anonwrite and os.getcwd() == zv.realpath: if self.warn_anonwrite and verbosity > 4 and os.getcwd() == zv.realpath:
t = "anyone can write to the current directory: {}\n" t = "anyone can write to the current directory: {}\n"
self.log(t.format(zv.realpath), c=1) self.log(t.format(zv.realpath), c=1)
@@ -2069,11 +2184,15 @@ class AuthSrv(object):
self.vfs = vfs self.vfs = vfs
self.acct = acct self.acct = acct
self.defpw = defpw
self.grps = grps self.grps = grps
self.iacct = {v: k for k, v in acct.items()} self.iacct = {v: k for k, v in acct.items()}
self.load_sessions()
self.re_pwd = None self.re_pwd = None
pwds = [re.escape(x) for x in self.iacct.keys()] pwds = [re.escape(x) for x in self.iacct.keys()]
pwds.extend(list(self.sesa))
if pwds: if pwds:
if self.ah.on: if self.ah.on:
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)" zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
@@ -2089,6 +2208,235 @@ class AuthSrv(object):
MIMES[ext] = mime MIMES[ext] = mime
EXTS.update({v: k for k, v in MIMES.items()}) EXTS.update({v: k for k, v in MIMES.items()})
if enshare:
# hide shares from controlpanel
vfs.all_vols = {
x: y
for x, y in vfs.all_vols.items()
if x != shr and not x.startswith(shrs)
}
assert db and cur and cur2 and shv # type: ignore
for row in cur.execute("select * from sh"):
s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row
shn = shv.nodes.get(s_k, None)
if not shn:
continue
try:
s_vfs, s_rem = vfs.get(
s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr
)
except Exception as ex:
t = "removing share [%s] by [%s] to [%s] due to %r"
self.log(t % (s_k, s_un, s_vp, ex), 3)
shv.nodes.pop(s_k)
continue
fns = []
if s_nf:
q = "select vp from sf where k = ?"
for (s_fn,) in cur2.execute(q, (s_k,)):
fns.append(s_fn)
shn.shr_files = set(fns)
shn.ls = shn._ls_shr
else:
shn.ls = shn._ls
shn.shr_src = (s_vfs, s_rem)
shn.realpath = s_vfs.canonical(s_rem)
if self.args.shr_v:
t = "mapped %s share [%s] by [%s] => [%s] => [%s]"
self.log(t % (s_pr, s_k, s_un, s_vp, shn.realpath))
# transplant shadowing into shares
for vn in shv.nodes.values():
svn, srem = vn.shr_src # type: ignore
if srem:
continue # free branch, safe
ap = svn.canonical(srem)
if bos.path.isfile(ap):
continue # also fine
for zs in svn.nodes.keys():
# hide subvolume
vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
cur2.close()
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"
if uname == "*" or uname not in self.defpw:
return False, "not logged in"
if uname in self.args.chpw_no:
return False, "not allowed for this account"
if len(pw) < self.args.chpw_len:
t = "minimum password length: %d characters"
return False, t % (self.args.chpw_len,)
hpw = self.ah.hash(pw) if self.ah.on else pw
if hpw == self.acct[uname]:
return False, "that's already your password my dude"
if hpw in self.iacct or hpw in self.sesa:
return False, "password is taken"
with self.mutex:
ap = self.args.chpw_db
if not bos.path.exists(ap):
pwdb = {}
else:
with open(ap, "r", encoding="utf-8") as f:
pwdb = json.load(f)
pwdb = [x for x in pwdb if x[0] != uname]
pwdb.append((uname, self.defpw[uname], hpw))
with open(ap, "w", encoding="utf-8") as f:
json.dump(pwdb, f, separators=(",\n", ": "))
self.log("reinitializing due to password-change for user [%s]" % (uname,))
if not broker:
# only true for tests
self._reload()
return True, "new password OK"
broker.ask("_reload_blocking", False, False).get()
return True, "new password OK"
def setup_chpw(self, acct: dict[str, str]) -> None:
ap = self.args.chpw_db
if not self.args.chpw or not bos.path.exists(ap):
return
with open(ap, "r", encoding="utf-8") as f:
pwdb = json.load(f)
useen = set()
urst = set()
uok = set()
for usr, orig, mod in pwdb:
useen.add(usr)
if usr not in acct:
# previous user, no longer known
continue
if acct[usr] != orig:
urst.add(usr)
continue
uok.add(usr)
acct[usr] = mod
if not self.args.chpw_v:
return
for usr in acct:
if usr not in useen:
urst.add(usr)
for zs in uok:
urst.discard(zs)
if self.args.chpw_v == 1 or (self.args.chpw_v == 2 and not urst):
t = "chpw: %d changed, %d unchanged"
self.log(t % (len(uok), len(urst)))
return
elif self.args.chpw_v == 2:
t = "chpw: %d changed" % (len(uok))
if urst:
t += ", \033[0munchanged:\033[35m %s" % (", ".join(list(urst)))
self.log(t, 6)
return
msg = ""
if uok:
t = "\033[0mchanged: \033[32m%s"
msg += t % (", ".join(list(uok)),)
if urst:
t = "%s\033[0munchanged: \033[35m%s"
msg += t % (
", " if msg else "",
", ".join(list(urst)),
)
self.log("chpw: " + msg, 6)
def setup_pwhash(self, acct: dict[str, str]) -> None: def setup_pwhash(self, acct: dict[str, str]) -> None:
self.ah = PWHash(self.args) self.ah = PWHash(self.args)
if not self.ah.on: if not self.ah.on:

View File

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

View File

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

View File

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

View File

@@ -12,8 +12,8 @@ def vf_bmap() -> dict[str, str]:
"dav_auth": "davauth", "dav_auth": "davauth",
"dav_rt": "davrt", "dav_rt": "davrt",
"ed": "dots", "ed": "dots",
"never_symlink": "neversymlink", "hardlink_only": "hardlinkonly",
"no_dedup": "copydupes", "no_dirsz": "nodirsz",
"no_dupe": "nodupe", "no_dupe": "nodupe",
"no_forget": "noforget", "no_forget": "noforget",
"no_pipe": "nopipe", "no_pipe": "nopipe",
@@ -23,6 +23,7 @@ def vf_bmap() -> dict[str, str]:
"no_athumb": "dathumb", "no_athumb": "dathumb",
} }
for k in ( for k in (
"dedup",
"dotsrch", "dotsrch",
"e2d", "e2d",
"e2ds", "e2ds",
@@ -58,6 +59,7 @@ def vf_vmap() -> dict[str, str]:
"no_hash": "nohash", "no_hash": "nohash",
"no_idx": "noidx", "no_idx": "noidx",
"re_maxage": "scan", "re_maxage": "scan",
"safe_dedup": "safededup",
"th_convt": "convt", "th_convt": "convt",
"th_size": "thsize", "th_size": "thsize",
"th_crop": "crop", "th_crop": "crop",
@@ -129,10 +131,11 @@ permdescs = {
flagcats = { flagcats = {
"uploads, general": { "uploads, general": {
"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",
"nodupe": "rejects existing files (instead of symlinking them)", "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",
"sparse": "force use of sparse files, mainly for s3-backed storage", "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", "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", "nosub": "forces all uploads into the top folder of the vfs",
@@ -159,7 +162,7 @@ flagcats = {
"lifetime=3600": "uploads are deleted after 1 hour", "lifetime=3600": "uploads are deleted after 1 hour",
}, },
"database, general": { "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", "e2ds": "scan writable folders for new files on startup; also sets -e2d",
"e2dsa": "scans all 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", "e2t": "enable multimedia indexing; makes it possible to search for tags",
@@ -177,7 +180,7 @@ flagcats = {
"noforget": "don't forget files when deleted from disk", "noforget": "don't forget files when deleted from disk",
"fat32": "avoid excessive reindexing on android sdcardfs", "fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff", "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", "xdev": "do not descend into other filesystems",
"xvol": "do not follow symlinks leaving the volume root", "xvol": "do not follow symlinks leaving the volume root",
"dotsrch": "show dotfiles in search results", "dotsrch": "show dotfiles in search results",

View File

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

View File

@@ -2,7 +2,6 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse # typechk import argparse # typechk
import base64
import calendar import calendar
import copy import copy
import errno import errno
@@ -45,6 +44,7 @@ from .util import unquote # type: ignore
from .util import ( from .util import (
APPLESAN_RE, APPLESAN_RE,
BITNESS, BITNESS,
HAVE_SQLITE3,
HTTPCODE, HTTPCODE,
META_NOBOTS, META_NOBOTS,
UTC, UTC,
@@ -57,6 +57,7 @@ from .util import (
absreal, absreal,
alltrace, alltrace,
atomic_move, atomic_move,
b64dec,
exclude_dotfiles, exclude_dotfiles,
formatdate, formatdate,
fsenc, fsenc,
@@ -86,6 +87,7 @@ from .util import (
relchk, relchk,
ren_open, ren_open,
runhook, runhook,
s2hms,
s3enc, s3enc,
sanitize_fn, sanitize_fn,
sanitize_vpath, sanitize_vpath,
@@ -126,7 +128,7 @@ class HttpCli(object):
""" """
def __init__(self, conn: "HttpConn") -> None: def __init__(self, conn: "HttpConn") -> None:
assert conn.sr assert conn.sr # !rm
self.t0 = time.time() self.t0 = time.time()
self.conn = conn self.conn = conn
@@ -204,7 +206,8 @@ class HttpCli(object):
def unpwd(self, m: Match[str]) -> str: def unpwd(self, m: Match[str]) -> str:
a, b, c = m.groups() a, b, c = m.groups()
return "%s\033[7m %s \033[27m%s" % (a, self.asrv.iacct[b], c) uname = self.asrv.iacct.get(b) or self.asrv.sesa.get(b)
return "%s\033[7m %s \033[27m%s" % (a, uname, c)
def _check_nonfatal(self, ex: Pebkac, post: bool) -> bool: def _check_nonfatal(self, ex: Pebkac, post: bool) -> bool:
if post: if post:
@@ -454,7 +457,7 @@ class HttpCli(object):
t = "incorrect --rp-loc or webserver config; expected vpath starting with [{}] but got [{}]" t = "incorrect --rp-loc or webserver config; expected vpath starting with [{}] but got [{}]"
self.log(t.format(self.args.R, vpath), 1) self.log(t.format(self.args.R, vpath), 1)
self.ouparam = {k: zs for k, zs in uparam.items()} self.ouparam = uparam.copy()
if self.args.rsp_slp: if self.args.rsp_slp:
time.sleep(self.args.rsp_slp) time.sleep(self.args.rsp_slp)
@@ -463,6 +466,9 @@ class HttpCli(object):
zso = self.headers.get("cookie") zso = self.headers.get("cookie")
if zso: if zso:
if len(zso) > 8192:
self.loud_reply("cookie header too big", status=400)
return False
zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x] zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll} cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll}
cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or "" cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or ""
@@ -497,9 +503,11 @@ class HttpCli(object):
): ):
try: try:
zb = zso.split(" ")[1].encode("ascii") zb = zso.split(" ")[1].encode("ascii")
zs = base64.b64decode(zb).decode("utf-8") zs = b64dec(zb).decode("utf-8")
# try "pwd", "x:pwd", "pwd:x" # try "pwd", "x:pwd", "pwd:x"
for bauth in [zs] + zs.split(":", 1)[::-1]: for bauth in [zs] + zs.split(":", 1)[::-1]:
if bauth in self.asrv.sesa:
break
hpw = self.asrv.ah.hash(bauth) hpw = self.asrv.ah.hash(bauth)
if self.asrv.iacct.get(hpw): if self.asrv.iacct.get(hpw):
break break
@@ -561,7 +569,11 @@ class HttpCli(object):
self.uname = "*" self.uname = "*"
else: else:
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
self.uname = self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) or "*" self.uname = (
self.asrv.sesa.get(self.pw)
or self.asrv.iacct.get(self.asrv.ah.hash(self.pw))
or "*"
)
self.rvol = self.asrv.vfs.aread[self.uname] self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname] self.wvol = self.asrv.vfs.awrite[self.uname]
@@ -968,10 +980,10 @@ class HttpCli(object):
status: int = 200, status: int = 200,
use302: bool = False, use302: bool = False,
) -> bool: ) -> bool:
vp = self.args.RS + vpath vp = self.args.SRS + vpath
html = self.j2s( html = self.j2s(
"msg", "msg",
h2='<a href="/{}">{} /{}</a>'.format( h2='<a href="{}">{} {}</a>'.format(
quotep(vp) + suf, flavor, html_escape(vp, crlf=True) + suf quotep(vp) + suf, flavor, html_escape(vp, crlf=True) + suf
), ),
pre=msg, pre=msg,
@@ -979,7 +991,7 @@ class HttpCli(object):
).encode("utf-8", "replace") ).encode("utf-8", "replace")
if use302: if use302:
self.reply(html, status=302, headers={"Location": "/" + vpath}) self.reply(html, status=302, headers={"Location": vp})
else: else:
self.reply(html, status=status) self.reply(html, status=status)
@@ -1141,7 +1153,7 @@ class HttpCli(object):
if "move" in self.uparam: if "move" in self.uparam:
return self.handle_mv() return self.handle_mv()
if not self.vpath: if not self.vpath and self.ouparam:
if "reload" in self.uparam: if "reload" in self.uparam:
return self.handle_reload() return self.handle_reload()
@@ -1163,23 +1175,12 @@ class HttpCli(object):
if "hc" in self.uparam: if "hc" in self.uparam:
return self.tx_svcs() return self.tx_svcs()
if "shares" in self.uparam:
return self.tx_shares()
if "h" in self.uparam: if "h" in self.uparam:
return self.tx_mounts() return self.tx_mounts()
# conditional redirect to single volumes
if not self.vpath and not self.ouparam:
nread = len(self.rvol)
nwrite = len(self.wvol)
if nread + nwrite == 1 or (self.rvol == self.wvol and nread == 1):
if nread == 1:
vpath = self.rvol[0]
else:
vpath = self.wvol[0]
if self.vpath != vpath:
self.redirect(vpath, flavor="redirecting to", use302=True)
return True
return self.tx_browser() return self.tx_browser()
def handle_propfind(self) -> bool: def handle_propfind(self) -> bool:
@@ -1395,7 +1396,7 @@ class HttpCli(object):
xroot = mkenod("D:orz") xroot = mkenod("D:orz")
xroot.insert(0, parse_xml(txt)) xroot.insert(0, parse_xml(txt))
xprop = xroot.find(r"./{DAV:}propertyupdate/{DAV:}set/{DAV:}prop") xprop = xroot.find(r"./{DAV:}propertyupdate/{DAV:}set/{DAV:}prop")
assert xprop assert xprop # !rm
for ze in xprop: for ze in xprop:
ze.clear() ze.clear()
@@ -1403,12 +1404,12 @@ class HttpCli(object):
xroot = parse_xml(txt) xroot = parse_xml(txt)
el = xroot.find(r"./{DAV:}response") el = xroot.find(r"./{DAV:}response")
assert el assert el # !rm
e2 = mktnod("D:href", quotep(self.args.SRS + self.vpath)) e2 = mktnod("D:href", quotep(self.args.SRS + self.vpath))
el.insert(0, e2) el.insert(0, e2)
el = xroot.find(r"./{DAV:}response/{DAV:}propstat") el = xroot.find(r"./{DAV:}response/{DAV:}propstat")
assert el assert el # !rm
el.insert(0, xprop) el.insert(0, xprop)
ret = '<?xml version="1.0" encoding="{}"?>\n'.format(uenc) ret = '<?xml version="1.0" encoding="{}"?>\n'.format(uenc)
@@ -1618,6 +1619,9 @@ class HttpCli(object):
if "delete" in self.uparam: if "delete" in self.uparam:
return self.handle_rm([]) return self.handle_rm([])
if "eshare" in self.uparam:
return self.handle_eshare()
if "application/octet-stream" in ctype: if "application/octet-stream" in ctype:
return self.handle_post_binary() return self.handle_post_binary()
@@ -1789,7 +1793,7 @@ class HttpCli(object):
fn = os.devnull fn = os.devnull
params.update(open_ka) params.update(open_ka)
assert fn assert fn # !rm
if not self.args.nw: if not self.args.nw:
if rnd: if rnd:
@@ -1861,10 +1865,12 @@ class HttpCli(object):
# small toctou, but better than clobbering a hardlink # small toctou, but better than clobbering a hardlink
wunlink(self.log, path, vfs.flags) wunlink(self.log, path, vfs.flags)
with ren_open(fn, *open_a, **params) as zfw: f, fn = ren_open(fn, *open_a, **params)
f, fn = zfw["orz"] try:
path = os.path.join(fdir, fn) path = os.path.join(fdir, fn)
post_sz, sha_hex, sha_b64 = hashcopy(reader, f, self.args.s_wr_slp) post_sz, sha_hex, sha_b64 = hashcopy(reader, f, self.args.s_wr_slp)
finally:
f.close()
if lim: if lim:
lim.nup(self.ip) lim.nup(self.ip)
@@ -1903,8 +1909,8 @@ class HttpCli(object):
fn2 = fn.rsplit(".", 1)[0] + "." + ext fn2 = fn.rsplit(".", 1)[0] + "." + ext
params["suffix"] = suffix[:-4] params["suffix"] = suffix[:-4]
with ren_open(fn, *open_a, **params) as zfw: f, fn2 = ren_open(fn2, *open_a, **params)
f, fn = zfw["orz"] f.close()
path2 = os.path.join(fdir, fn2) path2 = os.path.join(fdir, fn2)
atomic_move(self.log, path, path2, vfs.flags) atomic_move(self.log, path, path2, vfs.flags)
@@ -2089,10 +2095,16 @@ class HttpCli(object):
if act == "zip": if act == "zip":
return self.handle_zip_post() return self.handle_zip_post()
if act == "chpw":
return self.handle_chpw()
if act == "logout":
return self.handle_logout()
raise Pebkac(422, 'invalid action "{}"'.format(act)) raise Pebkac(422, 'invalid action "{}"'.format(act))
def handle_zip_post(self) -> bool: def handle_zip_post(self) -> bool:
assert self.parser assert self.parser # !rm
try: try:
k = next(x for x in self.uparam if x in ("zip", "tar")) k = next(x for x in self.uparam if x in ("zip", "tar"))
except: except:
@@ -2147,6 +2159,9 @@ class HttpCli(object):
if "srch" in self.uparam or "srch" in body: if "srch" in self.uparam or "srch" in body:
return self.handle_search(body) return self.handle_search(body)
if "share" in self.uparam:
return self.handle_share(body)
if "delete" in self.uparam: if "delete" in self.uparam:
return self.handle_rm(body) return self.handle_rm(body)
@@ -2203,7 +2218,9 @@ class HttpCli(object):
def handle_search(self, body: dict[str, Any]) -> bool: def handle_search(self, body: dict[str, Any]) -> bool:
idx = self.conn.get_u2idx() idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"): if not idx or not hasattr(idx, "p_end"):
raise Pebkac(500, "server busy, or sqlite3 not available; cannot search") if not HAVE_SQLITE3:
raise Pebkac(500, "sqlite3 not found on server; search is disabled")
raise Pebkac(500, "server busy, cannot search; please retry in a bit")
vols: list[VFS] = [] vols: list[VFS] = []
seen: dict[VFS, bool] = {} seen: dict[VFS, bool] = {}
@@ -2287,11 +2304,16 @@ class HttpCli(object):
vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True) vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True)
ptop = (vfs.dbv or vfs).realpath ptop = (vfs.dbv or vfs).realpath
x = self.conn.hsrv.broker.ask("up2k.handle_chunks", ptop, wark, chashes) broker = self.conn.hsrv.broker
x = broker.ask("up2k.handle_chunks", ptop, wark, chashes)
response = x.get() response = x.get()
chashes, chunksize, cstarts, path, lastmod, sprs = response chashes, chunksize, cstarts, path, lastmod, sprs = response
maxsize = chunksize * len(chashes) maxsize = chunksize * len(chashes)
cstart0 = cstarts[0] cstart0 = cstarts[0]
locked = chashes # remaining chunks to be received in this request
written = [] # chunks written to disk, but not yet released by up2k
num_left = -1 # num chunks left according to most recent up2k release
treport = time.time() # ratelimit up2k reporting to reduce overhead
try: try:
if self.args.nw: if self.args.nw:
@@ -2337,11 +2359,8 @@ class HttpCli(object):
remains -= chunksize remains -= chunksize
if len(cstart) > 1 and path != os.devnull: if len(cstart) > 1 and path != os.devnull:
self.log( t = " & ".join(unicode(x) for x in cstart[1:])
"clone {} to {}".format( self.log("clone %s to %s" % (cstart[0], t))
cstart[0], " & ".join(unicode(x) for x in cstart[1:])
)
)
ofs = 0 ofs = 0
while ofs < chunksize: while ofs < chunksize:
bufsz = max(4 * 1024 * 1024, self.args.iobuf) bufsz = max(4 * 1024 * 1024, self.args.iobuf)
@@ -2356,6 +2375,25 @@ class HttpCli(object):
self.log("clone {} done".format(cstart[0])) self.log("clone {} done".format(cstart[0]))
# be quick to keep the tcp winsize scale;
# if we can't confirm rn then that's fine
written.append(chash)
now = time.time()
if now - treport < 1:
continue
treport = now
x = broker.ask("up2k.fast_confirm_chunks", ptop, wark, written)
num_left, t = x.get()
if num_left < -1:
self.loud_reply(t, status=500)
locked = written = []
return False
elif num_left >= 0:
t = "got %d more chunks, %d left"
self.log(t % (len(written), num_left), 6)
locked = locked[len(written) :]
written = []
if not fpool: if not fpool:
f.close() f.close()
else: else:
@@ -2366,25 +2404,25 @@ class HttpCli(object):
f.close() f.close()
raise raise
finally: finally:
x = self.conn.hsrv.broker.ask("up2k.release_chunks", ptop, wark, chashes) if locked:
x.get() # block client until released # now block until all chunks released+confirmed
x = broker.ask("up2k.confirm_chunks", ptop, wark, locked)
x = self.conn.hsrv.broker.ask("up2k.confirm_chunks", ptop, wark, chashes) num_left, t = x.get()
ztis = x.get() if num_left < 0:
try: self.loud_reply(t, status=500)
num_left, fin_path = ztis
except:
self.loud_reply(ztis, status=500)
return False return False
t = "got %d more chunks, %d left"
self.log(t % (len(locked), num_left), 6)
if num_left < 0:
raise Pebkac(500, "unconfirmed; see serverlog")
if not num_left and fpool: if not num_left and fpool:
with self.u2mutex: with self.u2mutex:
self.u2fh.close(path) self.u2fh.close(path)
if not num_left and not self.args.nw: if not num_left and not self.args.nw:
self.conn.hsrv.broker.ask( broker.ask("up2k.finish_upload", ptop, wark, self.u2fh.aps).get()
"up2k.finish_upload", ptop, wark, self.u2fh.aps
).get()
cinf = self.headers.get("x-up2k-stat", "") cinf = self.headers.get("x-up2k-stat", "")
@@ -2393,8 +2431,25 @@ class HttpCli(object):
self.reply(b"thank") self.reply(b"thank")
return True return True
def handle_chpw(self) -> bool:
assert self.parser # !rm
pwd = self.parser.require("pw", 64)
self.parser.drop()
ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
if ok:
ok, msg = self.get_pwd_cookie(pwd)
if ok:
msg = "new password OK"
redir = (self.args.SRS + "?h") if ok else ""
h2 = '<a href="' + self.args.SRS + '?h">ack</a>'
html = self.j2s("msg", h1=msg, h2=h2, redir=redir)
self.reply(html.encode("utf-8"))
return True
def handle_login(self) -> bool: def handle_login(self) -> bool:
assert self.parser assert self.parser # !rm
pwd = self.parser.require("cppwd", 64) pwd = self.parser.require("cppwd", 64)
try: try:
uhash = self.parser.require("uhash", 256) uhash = self.parser.require("uhash", 256)
@@ -2402,9 +2457,8 @@ class HttpCli(object):
uhash = "" uhash = ""
self.parser.drop() self.parser.drop()
self.out_headerlist = [ if not pwd:
x for x in self.out_headerlist if x[0] != "Set-Cookie" or "cppw" != x[1][:4] raise Pebkac(422, "password cannot be blank")
]
dst = self.args.SRS dst = self.args.SRS
if self.vpath: if self.vpath:
@@ -2417,14 +2471,32 @@ class HttpCli(object):
dst += "&" if "?" in dst else "?" dst += "&" if "?" in dst else "?"
dst += "_=1#" + html_escape(uhash, True, True) dst += "_=1#" + html_escape(uhash, True, True)
msg = self.get_pwd_cookie(pwd) _, msg = self.get_pwd_cookie(pwd)
html = self.j2s("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst) html = self.j2s("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst)
self.reply(html.encode("utf-8")) self.reply(html.encode("utf-8"))
return True return True
def get_pwd_cookie(self, pwd: str) -> str: def handle_logout(self) -> bool:
assert self.parser # !rm
self.parser.drop()
self.log("logout " + self.uname)
self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
self.get_pwd_cookie("x")
dst = self.args.SRS + "?h"
h2 = '<a href="' + dst + '">ack</a>'
html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst)
self.reply(html.encode("utf-8"))
return True
def get_pwd_cookie(self, pwd: str) -> tuple[bool, str]:
uname = self.asrv.sesa.get(pwd)
if not uname:
hpwd = self.asrv.ah.hash(pwd) hpwd = self.asrv.ah.hash(pwd)
uname = self.asrv.iacct.get(hpwd) uname = self.asrv.iacct.get(hpwd)
if uname:
pwd = self.asrv.ases.get(uname) or pwd
if uname: if uname:
msg = "hi " + uname msg = "hi " + uname
dur = int(60 * 60 * self.args.logout) dur = int(60 * 60 * self.args.logout)
@@ -2434,8 +2506,9 @@ class HttpCli(object):
logpwd = "" logpwd = ""
elif self.args.log_badpwd == 2: elif self.args.log_badpwd == 2:
zb = hashlib.sha512(pwd.encode("utf-8", "replace")).digest() zb = hashlib.sha512(pwd.encode("utf-8", "replace")).digest()
logpwd = "%" + base64.b64encode(zb[:12]).decode("utf-8") logpwd = "%" + ub64enc(zb[:12]).decode("ascii")
if pwd != "x":
self.log("invalid password: {}".format(logpwd), 3) self.log("invalid password: {}".format(logpwd), 3)
self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords") self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")
@@ -2449,15 +2522,16 @@ class HttpCli(object):
for k in ("cppwd", "cppws") if self.is_https else ("cppwd",): for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
ck = gencookie(k, pwd, self.args.R, False) ck = gencookie(k, pwd, self.args.R, False)
self.out_headerlist.append(("Set-Cookie", ck)) self.out_headerlist.append(("Set-Cookie", ck))
self.out_headers.pop("Set-Cookie", None) # drop keepalive
else: else:
k = "cppws" if self.is_https else "cppwd" k = "cppws" if self.is_https else "cppwd"
ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly") ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly")
self.out_headerlist.append(("Set-Cookie", ck)) self.out_headers["Set-Cookie"] = ck
return msg return dur > 0, msg
def handle_mkdir(self) -> bool: def handle_mkdir(self) -> bool:
assert self.parser assert self.parser # !rm
new_dir = self.parser.require("name", 512) new_dir = self.parser.require("name", 512)
self.parser.drop() self.parser.drop()
@@ -2493,7 +2567,7 @@ class HttpCli(object):
except: except:
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
self.out_headers["X-New-Dir"] = quotep(vpath) self.out_headers["X-New-Dir"] = quotep(self.args.RS + vpath)
if dav: if dav:
self.reply(b"", 201) self.reply(b"", 201)
@@ -2503,7 +2577,7 @@ class HttpCli(object):
return True return True
def handle_new_md(self) -> bool: def handle_new_md(self) -> bool:
assert self.parser assert self.parser # !rm
new_file = self.parser.require("name", 512) new_file = self.parser.require("name", 512)
self.parser.drop() self.parser.drop()
@@ -2669,8 +2743,8 @@ class HttpCli(object):
bos.makedirs(fdir) bos.makedirs(fdir)
# reserve destination filename # reserve destination filename
with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as zfw: f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
fname = zfw["orz"][1] f.close()
tnam = fname + ".PARTIAL" tnam = fname + ".PARTIAL"
if self.args.dotpart: if self.args.dotpart:
@@ -2693,8 +2767,8 @@ class HttpCli(object):
v2 = lim.dfv - lim.dfl v2 = lim.dfv - lim.dfl
max_sz = min(v1, v2) if v1 and v2 else v1 or v2 max_sz = min(v1, v2) if v1 and v2 else v1 or v2
with ren_open(tnam, "wb", self.args.iobuf, **open_args) as zfw: f, tnam = ren_open(tnam, "wb", self.args.iobuf, **open_args)
f, tnam = zfw["orz"] try:
tabspath = os.path.join(fdir, tnam) tabspath = os.path.join(fdir, tnam)
self.log("writing to {}".format(tabspath)) self.log("writing to {}".format(tabspath))
sz, sha_hex, sha_b64 = hashcopy( sz, sha_hex, sha_b64 = hashcopy(
@@ -2702,6 +2776,8 @@ class HttpCli(object):
) )
if sz == 0: if sz == 0:
raise Pebkac(400, "empty files in post") raise Pebkac(400, "empty files in post")
finally:
f.close()
if lim: if lim:
lim.nup(self.ip) lim.nup(self.ip)
@@ -2911,7 +2987,7 @@ class HttpCli(object):
return True return True
def handle_text_upload(self) -> bool: def handle_text_upload(self) -> bool:
assert self.parser assert self.parser # !rm
try: try:
cli_lastmod3 = int(self.parser.require("lastmod", 16)) cli_lastmod3 = int(self.parser.require("lastmod", 16))
except: except:
@@ -2996,7 +3072,7 @@ class HttpCli(object):
pass pass
wrename(self.log, fp, os.path.join(mdir, ".hist", mfile2), vfs.flags) wrename(self.log, fp, os.path.join(mdir, ".hist", mfile2), vfs.flags)
assert self.parser.gen assert self.parser.gen # !rm
p_field, _, p_data = next(self.parser.gen) p_field, _, p_data = next(self.parser.gen)
if p_field != "body": if p_field != "body":
raise Pebkac(400, "expected body, got {}".format(p_field)) raise Pebkac(400, "expected body, got {}".format(p_field))
@@ -3097,7 +3173,7 @@ class HttpCli(object):
# some browser append "; length=573" # some browser append "; length=573"
cli_lastmod = cli_lastmod.split(";")[0].strip() cli_lastmod = cli_lastmod.split(";")[0].strip()
cli_dt = parsedate(cli_lastmod) cli_dt = parsedate(cli_lastmod)
assert cli_dt assert cli_dt # !rm
cli_ts = calendar.timegm(cli_dt) cli_ts = calendar.timegm(cli_dt)
return file_lastmod, int(file_ts) > int(cli_ts) return file_lastmod, int(file_ts) > int(cli_ts)
except Exception as ex: except Exception as ex:
@@ -3246,6 +3322,7 @@ class HttpCli(object):
raise Exception("not found in registry") raise Exception("not found in registry")
self.pipes.set(req_path, job) self.pipes.set(req_path, job)
except Exception as ex: except Exception as ex:
if getattr(ex, "errno", 0) != errno.ENOENT:
self.log("will not pipe [%s]; %s" % (ap_data, ex), 6) self.log("will not pipe [%s]; %s" % (ap_data, ex), 6)
ptop = None ptop = None
@@ -3864,6 +3941,9 @@ class HttpCli(object):
vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/") vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/")
pw = pw.replace(" ", "%20") pw = pw.replace(" ", "%20")
vp = vp.replace(" ", "%20") vp = vp.replace(" ", "%20")
if pw in self.asrv.sesa:
pw = "pwd"
html = self.j2s( html = self.j2s(
"svcs", "svcs",
args=self.args, args=self.args,
@@ -3888,11 +3968,30 @@ class HttpCli(object):
for y in [self.rvol, self.wvol, self.avol] for y in [self.rvol, self.wvol, self.avol]
] ]
if self.avol and not self.args.no_rescan: ups = []
x = self.conn.hsrv.broker.ask("up2k.get_state") now = time.time()
get_vst = self.avol and not self.args.no_rescan
get_ups = self.rvol and not self.args.no_up_list and self.uname or ""
if get_vst or get_ups:
x = self.conn.hsrv.broker.ask("up2k.get_state", get_vst, get_ups)
vs = json.loads(x.get()) vs = json.loads(x.get())
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()} vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()}
else: try:
for rem, sz, t0, poke, vp in vs["ups"]:
fdone = max(0.001, 1 - rem)
td = max(0.1, now - t0)
rd, fn = vsplit(vp.replace(os.sep, "/"))
if not rd:
rd = "/"
erd = quotep(rd)
rds = rd.replace("/", " / ")
spd = humansize(sz * fdone / td, True) + "/s"
eta = s2hms((td / fdone) - td, True)
idle = s2hms(now - poke, True)
ups.append((int(100 * fdone), spd, eta, idle, erd, rds, fn))
except Exception as ex:
self.log("failed to list upload progress: %r" % (ex,), 1)
if not get_vst:
vstate = {} vstate = {}
vs = { vs = {
"scanning": None, "scanning": None,
@@ -3917,6 +4016,12 @@ class HttpCli(object):
for k in ["scanning", "hashq", "tagq", "mtpq", "dbwt"]: for k in ["scanning", "hashq", "tagq", "mtpq", "dbwt"]:
txt += " {}({})".format(k, vs[k]) txt += " {}({})".format(k, vs[k])
if ups:
txt += "\n\nincoming files:"
for zt in ups:
txt += "\n%s" % (", ".join((str(x) for x in zt)),)
txt += "\n"
if rvol: if rvol:
txt += "\nyou can browse:" txt += "\nyou can browse:"
for v in rvol: for v in rvol:
@@ -3938,7 +4043,9 @@ class HttpCli(object):
rvol=rvol, rvol=rvol,
wvol=wvol, wvol=wvol,
avol=avol, avol=avol,
in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]),
vstate=vstate, vstate=vstate,
ups=ups,
scanning=vs["scanning"], scanning=vs["scanning"],
hashq=vs["hashq"], hashq=vs["hashq"],
tagq=vs["tagq"], tagq=vs["tagq"],
@@ -3948,6 +4055,7 @@ class HttpCli(object):
k304=self.k304(), k304=self.k304(),
k304vis=self.args.k304 > 0, k304vis=self.args.k304 > 0,
ver=S_VERSION if self.args.ver else "", ver=S_VERSION if self.args.ver else "",
chpw=self.args.chpw and self.uname != "*",
ahttps="" if self.is_https else "https://" + self.host + self.req, ahttps="" if self.is_https else "https://" + self.host + self.req,
) )
self.reply(html.encode("utf-8")) self.reply(html.encode("utf-8"))
@@ -3985,10 +4093,10 @@ class HttpCli(object):
def tx_404(self, is_403: bool = False) -> bool: def tx_404(self, is_403: bool = False) -> bool:
rc = 404 rc = 404
if self.args.vague_403: if self.args.vague_403:
t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try logging in or <a href="{}/?h">go home</a></p>' t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try a password or <a href="{}/?h">go home</a></p>'
pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try logging in)" pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try a password)"
elif is_403: elif is_403:
t = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">you\'ll have to log in or <a href="{}/?h">go home</a></p>' t = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">use a password or <a href="{}/?h">go home</a></p>'
pt = "403 forbiddena ~┻━┻ (you'll have to log in)" pt = "403 forbiddena ~┻━┻ (you'll have to log in)"
rc = 403 rc = 403
else: else:
@@ -4005,7 +4113,8 @@ class HttpCli(object):
t = t.format(self.args.SR) t = t.format(self.args.SR)
qv = quotep(self.vpaths) + self.ourlq() qv = quotep(self.vpaths) + self.ourlq()
html = self.j2s("splash", this=self, qvpath=qv, msg=t) in_shr = self.args.shr and self.vpath.startswith(self.args.shr[1:])
html = self.j2s("splash", this=self, qvpath=qv, in_shr=in_shr, msg=t)
self.reply(html.encode("utf-8"), status=rc) self.reply(html.encode("utf-8"), status=rc)
return True return True
@@ -4082,7 +4191,9 @@ class HttpCli(object):
dst = dst[len(top) + 1 :] dst = dst[len(top) + 1 :]
ret = self.gen_tree(top, dst, self.uparam.get("k", "")) ret = self.gen_tree(top, dst, self.uparam.get("k", ""))
if self.is_vproxied: if self.is_vproxied and not self.uparam["tree"]:
# uparam is '' on initial load, which is
# the only time we gotta fill in the blanks
parents = self.args.R.split("/") parents = self.args.R.split("/")
for parent in reversed(parents): for parent in reversed(parents):
ret = {"k%s" % (parent,): ret, "a": []} ret = {"k%s" % (parent,): ret, "a": []}
@@ -4157,7 +4268,9 @@ class HttpCli(object):
def tx_ups(self) -> bool: def tx_ups(self) -> bool:
idx = self.conn.get_u2idx() idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"): if not idx or not hasattr(idx, "p_end"):
raise Pebkac(500, "sqlite3 is not available on the server; cannot unpost") if not HAVE_SQLITE3:
raise Pebkac(500, "sqlite3 not found on server; unpost is disabled")
raise Pebkac(500, "server busy, cannot unpost; please retry in a bit")
filt = self.uparam.get("filter") or "" filt = self.uparam.get("filter") or ""
lm = "ups [{}]".format(filt) lm = "ups [{}]".format(filt)
@@ -4246,6 +4359,187 @@ class HttpCli(object):
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json") self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True return True
def tx_shares(self) -> bool:
if self.uname == "*":
self.loud_reply("you're not logged in")
return True
idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"):
if not HAVE_SQLITE3:
raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
raise Pebkac(500, "server busy, cannot list shares; please retry in a bit")
cur = idx.get_shr()
if not cur:
raise Pebkac(400, "huh, sharing must be disabled in the server config...")
rows = cur.execute("select * from sh").fetchall()
rows = [list(x) for x in rows]
if self.uname != self.args.shr_adm:
rows = [x for x in rows if x[5] == self.uname]
for x in rows:
x[1] = "yes" if x[1] else ""
html = self.j2s(
"shares", this=self, shr=self.args.shr, rows=rows, now=int(time.time())
)
self.reply(html.encode("utf-8"), status=200)
return True
def handle_eshare(self) -> bool:
idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"):
if not HAVE_SQLITE3:
raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
if self.args.shr_v:
self.log("handle_eshare: " + self.req)
cur = idx.get_shr()
if not cur:
raise Pebkac(400, "huh, sharing must be disabled in the server config...")
skey = self.vpath.split("/")[-1]
rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
un = rows[0][0] if rows and rows[0] else ""
if not un:
raise Pebkac(400, "that sharekey didn't match anything")
expiry = rows[0][1]
if un != self.uname and self.uname != self.args.shr_adm:
t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin"
raise Pebkac(400, t % (self.uname, un))
reload = False
act = self.uparam["eshare"]
if act == "rm":
cur.execute("delete from sh where k = ?", (skey,))
if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
reload = True
else:
now = time.time()
if expiry < now:
expiry = now
reload = True
expiry += int(act) * 60
cur.execute("update sh set t1 = ? where k = ?", (expiry, skey))
cur.connection.commit()
if reload:
self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
self.redirect(self.args.SRS + "?shares")
return True
def handle_share(self, req: dict[str, str]) -> bool:
idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"):
if not HAVE_SQLITE3:
raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
if self.args.shr_v:
self.log("handle_share: " + json.dumps(req, indent=4))
skey = req["k"]
vps = req["vp"]
fns = []
if len(vps) == 1:
vp = vps[0]
if not vp.endswith("/"):
vp, zs = vp.rsplit("/", 1)
fns = [zs]
else:
for zs in vps:
if zs.endswith("/"):
t = "you cannot select more than one folder, or mix flies and folders in one selection"
raise Pebkac(400, t)
vp = vps[0].rsplit("/", 1)[0]
for zs in vps:
vp2, fn = zs.rsplit("/", 1)
fns.append(fn)
if vp != vp2:
t = "mismatching base paths in selection:\n [%s]\n [%s]"
raise Pebkac(400, t % (vp, vp2))
vp = vp.strip("/")
if self.is_vproxied and (vp == self.args.R or vp.startswith(self.args.RS)):
vp = vp[len(self.args.RS) :]
m = re.search(r"([^0-9a-zA-Z_-])", skey)
if m:
raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
if vp.startswith(self.args.shr[1:]):
raise Pebkac(400, "yo dawg...")
cur = idx.get_shr()
if not cur:
raise Pebkac(400, "huh, sharing must be disabled in the server config...")
q = "select * from sh where k = ?"
qr = cur.execute(q, (skey,)).fetchall()
if qr and qr[0]:
self.log("sharekey taken by %r" % (qr,))
raise Pebkac(400, "sharekey [%s] is already in use" % (skey,))
# ensure user has requested perms
s_rd = "read" in req["perms"]
s_wr = "write" in req["perms"]
s_mv = "move" in req["perms"]
s_del = "delete" in req["perms"]
try:
vfs, rem = self.asrv.vfs.get(vp, self.uname, s_rd, s_wr, s_mv, s_del)
except:
raise Pebkac(400, "you dont have all the perms you tried to grant")
ap, reals, _ = vfs.ls(
rem, self.uname, not self.args.no_scandir, [[s_rd, s_wr, s_mv, s_del]]
)
rfns = set([x[0] for x in reals])
for fn in fns:
if fn not in rfns:
raise Pebkac(400, "selected file not found on disk: [%s]" % (fn,))
pw = req.get("pw") or ""
now = int(time.time())
sexp = req["exp"]
exp = int(sexp) if sexp else 0
exp = now + exp * 60 if exp else 0
pr = "".join(zc for zc, zb in zip("rwmd", (s_rd, s_wr, s_mv, s_del)) if zb)
q = "insert into sh values (?,?,?,?,?,?,?,?)"
cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp))
q = "insert into sf values (?,?)"
for fn in fns:
cur.execute(q, (skey, fn))
cur.connection.commit()
self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
fn = quotep(fns[0]) if len(fns) == 1 else ""
surl = "created share: %s://%s%s%s%s/%s" % (
"https" if self.is_https else "http",
self.host,
self.args.SR,
self.args.shr,
skey,
fn,
)
self.loud_reply(surl, status=201)
return True
def handle_rm(self, req: list[str]) -> bool: def handle_rm(self, req: list[str]) -> bool:
if not req and not self.can_delete: if not req and not self.can_delete:
raise Pebkac(403, "not allowed for user " + self.uname) raise Pebkac(403, "not allowed for user " + self.uname)
@@ -4644,6 +4938,7 @@ class HttpCli(object):
"have_mv": (not self.args.no_mv), "have_mv": (not self.args.no_mv),
"have_del": (not self.args.no_del), "have_del": (not self.args.no_del),
"have_zip": (not self.args.no_zip), "have_zip": (not self.args.no_zip),
"have_shr": self.args.shr,
"have_unpost": int(self.args.unpost), "have_unpost": int(self.args.unpost),
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"), "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
"dgrid": "grid" in vf, "dgrid": "grid" in vf,
@@ -4855,7 +5150,6 @@ class HttpCli(object):
dirs.append(item) dirs.append(item)
else: else:
files.append(item) files.append(item)
item["rd"] = rem
if is_dk and not vf.get("dks"): if is_dk and not vf.get("dks"):
dirs = [] dirs = []
@@ -4878,16 +5172,10 @@ class HttpCli(object):
add_up_at = ".up_at" in mte add_up_at = ".up_at" in mte
is_admin = self.can_admin is_admin = self.can_admin
tagset: set[str] = set() tagset: set[str] = set()
for fe in files: rd = vrem
for fe in files if icur else []:
assert icur # !rm
fn = fe["name"] fn = fe["name"]
rd = fe["rd"]
del fe["rd"]
if not icur:
continue
if vn != dbv:
_, rd = vn.get_dbv(rd)
erd_efn = (rd, fn) erd_efn = (rd, fn)
q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'" q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'"
try: try:
@@ -4929,13 +5217,25 @@ class HttpCli(object):
fe["tags"] = tags fe["tags"] = tags
if icur: if icur:
for fe in dirs:
fe["tags"] = ODict()
lmte = list(mte) lmte = list(mte)
if self.can_admin: if self.can_admin:
lmte.extend(("up_ip", ".up_at")) lmte.extend(("up_ip", ".up_at"))
taglist = [k for k in lmte if k in tagset] if "nodirsz" not in vf:
tagset.add(".files")
vdir = "%s/" % (rd,) if rd else ""
q = "select sz, nf from ds where rd=? limit 1"
for fe in dirs: for fe in dirs:
fe["tags"] = ODict() try:
hit = icur.execute(q, (vdir + fe["name"],)).fetchone()
(fe["sz"], fe["tags"][".files"]) = hit
except:
pass # 404 or mojibake
taglist = [k for k in lmte if k in tagset]
else: else:
taglist = list(tagset) taglist = list(tagset)
@@ -5079,7 +5379,7 @@ class HttpCli(object):
fmt = vn.flags.get("og_th", "j") fmt = vn.flags.get("og_th", "j")
th_base = ujoin(url_base, quotep(thumb)) th_base = ujoin(url_base, quotep(thumb))
query = "th=%s&cache" % (fmt,) query = "th=%s&cache" % (fmt,)
query = ub64enc(query.encode("utf-8")).decode("utf-8") query = ub64enc(query.encode("utf-8")).decode("ascii")
# discord looks at file extension, not content-type... # discord looks at file extension, not content-type...
query += "/th.jpg" if "j" in fmt else "/th.webp" query += "/th.jpg" if "j" in fmt else "/th.webp"
j2a["og_thumb"] = "%s/.uqe/%s" % (th_base, query) j2a["og_thumb"] = "%s/.uqe/%s" % (th_base, query)
@@ -5088,7 +5388,7 @@ class HttpCli(object):
j2a["og_file"] = file j2a["og_file"] = file
if og_fn: if og_fn:
og_fn_q = quotep(og_fn) og_fn_q = quotep(og_fn)
query = ub64enc(b"raw").decode("utf-8") query = ub64enc(b"raw").decode("ascii")
query += "/%s" % (og_fn_q,) query += "/%s" % (og_fn_q,)
j2a["og_url"] = ujoin(url_base, og_fn_q) j2a["og_url"] = ujoin(url_base, og_fn_q)
j2a["og_raw"] = j2a["og_url"] + "/.uqe/" + query j2a["og_raw"] = j2a["og_url"] + "/.uqe/" + query

View File

@@ -190,7 +190,7 @@ class HttpConn(object):
if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"): if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"):
ciphers = self.s.shared_ciphers() ciphers = self.s.shared_ciphers()
assert ciphers assert ciphers # !rm
overlap = [str(y[::-1]) for y in ciphers] overlap = [str(y[::-1]) for y in ciphers]
self.log("TLS cipher overlap:" + "\n".join(overlap)) self.log("TLS cipher overlap:" + "\n".join(overlap))
for k, v in [ for k, v in [

View File

@@ -1,7 +1,6 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import base64
import math import math
import os import os
import re import re
@@ -75,6 +74,7 @@ from .util import (
spack, spack,
start_log_thrs, start_log_thrs,
start_stackmon, start_stackmon,
ub64enc,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -154,7 +154,17 @@ class HttpSrv(object):
env = jinja2.Environment() env = jinja2.Environment()
env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web")) env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"] jn = [
"splash",
"shares",
"svcs",
"browser",
"browser2",
"msg",
"md",
"mde",
"cf",
]
self.j2 = {x: env.get_template(x + ".html") for x in jn} self.j2 = {x: env.get_template(x + ".html") for x in jn}
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz") zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
self.prism = os.path.exists(zs) self.prism = os.path.exists(zs)
@@ -227,7 +237,7 @@ class HttpSrv(object):
if self.args.log_htp: if self.args.log_htp:
self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6) self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6)
assert self.tp_q assert self.tp_q # !rm
for _ in range(n): for _ in range(n):
self.tp_q.put(None) self.tp_q.put(None)
@@ -365,7 +375,7 @@ class HttpSrv(object):
cip = cip[7:] cip = cip[7:]
addr = (cip, saddr[1]) addr = (cip, saddr[1])
else: else:
addr = (ip, sck.fileno()) addr = ("127.8.3.7", sck.fileno())
except (OSError, socket.error) as ex: except (OSError, socket.error) as ex:
if self.stopping: if self.stopping:
break break
@@ -421,7 +431,7 @@ class HttpSrv(object):
) )
def thr_poolw(self) -> None: def thr_poolw(self) -> None:
assert self.tp_q assert self.tp_q # !rm
while True: while True:
task = self.tp_q.get() task = self.tp_q.get()
if not task: if not task:
@@ -533,8 +543,8 @@ class HttpSrv(object):
except: except:
pass pass
v = base64.urlsafe_b64encode(spack(b">xxL", int(v))) # spack gives 4 lsb, take 3 lsb, get 4 ch
self.cb_v = v.decode("ascii")[-4:] self.cb_v = ub64enc(spack(b">L", int(v))[1:]).decode("ascii")
self.cb_ts = time.time() self.cb_ts = time.time()
return self.cb_v return self.cb_v

View File

@@ -2,8 +2,6 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse import argparse
import base64
import calendar
import errno import errno
import gzip import gzip
import logging import logging
@@ -16,7 +14,7 @@ import string
import sys import sys
import threading import threading
import time import time
from datetime import datetime, timedelta from datetime import datetime
# from inspect import currentframe # from inspect import currentframe
# print(currentframe().f_lineno) # print(currentframe().f_lineno)
@@ -68,6 +66,7 @@ from .util import (
pybin, pybin,
start_log_thrs, start_log_thrs,
start_stackmon, start_stackmon,
ub64enc,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -104,8 +103,10 @@ class SvcHub(object):
self.argv = argv self.argv = argv
self.E: EnvParams = args.E self.E: EnvParams = args.E
self.no_ansi = args.no_ansi self.no_ansi = args.no_ansi
self.tz = UTC if args.log_utc else None
self.logf: Optional[typing.TextIO] = None self.logf: Optional[typing.TextIO] = None
self.logf_base_fn = "" self.logf_base_fn = ""
self.is_dut = False # running in unittest; always False
self.stop_req = False self.stop_req = False
self.stopping = False self.stopping = False
self.stopped = False self.stopped = False
@@ -117,7 +118,8 @@ class SvcHub(object):
self.httpsrv_up = 0 self.httpsrv_up = 0
self.log_mutex = threading.Lock() self.log_mutex = threading.Lock()
self.next_day = 0 self.cday = 0
self.cmon = 0
self.tstack = 0.0 self.tstack = 0.0
self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8) self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
@@ -208,6 +210,23 @@ class SvcHub(object):
t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance" t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance"
self.log("root", t % (args.s_rd_sz, args.iobuf), 3) self.log("root", t % (args.s_rd_sz, args.iobuf), 3)
if args.chpw and args.idp_h_usr:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
self.log("root", t, 1)
raise Exception(t)
noch = set()
for zs in args.chpw_no or []:
zsl = [x.strip() for x in zs.split(",")]
noch.update([x for x in zsl if x])
args.chpw_no = noch
if not self.args.no_ses:
self.setup_session_db()
if args.shr:
self.setup_share_db()
bri = "zy"[args.theme % 2 :][:1] bri = "zy"[args.theme % 2 :][:1]
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)] ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
args.theme = "{0}{1} {0} {1}".format(ch, bri) args.theme = "{0}{1} {0} {1}".format(ch, bri)
@@ -353,6 +372,151 @@ class SvcHub(object):
self.broker = Broker(self) 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:
self.log("root", "sqlite3 not available; disabling --shr", 1)
al.shr = ""
return
import sqlite3
al.shr = al.shr.strip("/")
if "/" in al.shr or not al.shr:
t = "config error: --shr must be the name of a virtual toplevel directory to put shares inside"
self.log("root", t, 1)
raise Exception(t)
al.shr = "/%s/" % (al.shr,)
create = True
modified = False
db_path = self.args.shr_db
self.log("root", "opening shares-db %s" % (db_path,))
for n in range(2):
try:
db = sqlite3.connect(db_path)
cur = db.cursor()
try:
cur.execute("select count(*) from sh").fetchone()
create = False
break
except:
pass
except Exception as ex:
if n:
raise
t = "shares-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)
sch1 = [
r"create table kv (k text, v int)",
r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",
# sharekey, password, src, perms, numFiles, owner, created, expires
]
sch2 = [
r"create table sf (k text, vp text)",
r"create index sf_k on sf(k)",
r"create index sh_k on sh(k)",
r"create index sh_t1 on sh(t1)",
]
assert db # type: ignore # !rm
assert cur # type: ignore # !rm
if create:
dver = 2
modified = True
for cmd in sch1 + sch2:
cur.execute(cmd)
self.log("root", "created new shares-db")
else:
(dver,) = cur.execute("select v from kv where k = 'sver'").fetchall()[0]
if dver == 1:
modified = True
for cmd in sch2:
cur.execute(cmd)
cur.execute("update sh set st = 0")
self.log("root", "shares-db schema upgrade ok")
if modified:
for cmd in [
r"delete from kv where k = 'sver'",
r"insert into kv values ('sver', %d)" % (2,),
]:
cur.execute(cmd)
db.commit()
cur.close()
db.close()
def start_ftpd(self) -> None: def start_ftpd(self) -> None:
time.sleep(30) time.sleep(30)
@@ -442,7 +606,7 @@ class SvcHub(object):
fng = [] fng = []
t_ff = "transcode audio, create spectrograms, video thumbnails" t_ff = "transcode audio, create spectrograms, video thumbnails"
to_check = [ 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_PIL, "pillow", "image thumbnails (plenty fast)"),
(HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"), (HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"),
(HAVE_WEBP, "pillow-webp", "create thumbnails as webp files"), (HAVE_WEBP, "pillow-webp", "create thumbnails as webp files"),
@@ -689,7 +853,7 @@ class SvcHub(object):
self.args.nc = min(self.args.nc, soft // 2) self.args.nc = min(self.args.nc, soft // 2)
def _logname(self) -> str: def _logname(self) -> str:
dt = datetime.now(UTC) dt = datetime.now(self.tz)
fn = str(self.args.lo) fn = str(self.args.lo)
for fs in "YmdHMS": for fs in "YmdHMS":
fs = "%" + fs fs = "%" + fs
@@ -815,18 +979,21 @@ class SvcHub(object):
Daemon(self._reload, "reloading") Daemon(self._reload, "reloading")
return "reload initiated" return "reload initiated"
def _reload(self, rescan_all_vols: bool = True) -> None: def _reload(self, rescan_all_vols: bool = True, up2k: bool = True) -> None:
with self.up2k.mutex: with self.up2k.mutex:
if self.reloading != 1: if self.reloading != 1:
return return
self.reloading = 2 self.reloading = 2
self.log("root", "reloading config") self.log("root", "reloading config")
self.asrv.reload() self.asrv.reload(9 if up2k else 4)
if up2k:
self.up2k.reload(rescan_all_vols) self.up2k.reload(rescan_all_vols)
else:
self.log("root", "reload done")
self.broker.reload() self.broker.reload()
self.reloading = 0 self.reloading = 0
def _reload_blocking(self, rescan_all_vols: bool = True) -> None: def _reload_blocking(self, rescan_all_vols: bool = True, up2k: bool = True) -> None:
while True: while True:
with self.up2k.mutex: with self.up2k.mutex:
if self.reloading < 2: if self.reloading < 2:
@@ -837,7 +1004,12 @@ class SvcHub(object):
# try to handle multiple pending IdP reloads at once: # try to handle multiple pending IdP reloads at once:
time.sleep(0.2) time.sleep(0.2)
self._reload(rescan_all_vols=rescan_all_vols) 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: def stop_thr(self) -> None:
while not self.stop_req: while not self.stop_req:
@@ -959,12 +1131,12 @@ class SvcHub(object):
return return
with self.log_mutex: with self.log_mutex:
zd = datetime.now(UTC) dt = datetime.now(self.tz)
ts = self.log_dfmt % ( ts = self.log_dfmt % (
zd.year, dt.year,
zd.month * 100 + zd.day, dt.month * 100 + dt.day,
(zd.hour * 100 + zd.minute) * 100 + zd.second, (dt.hour * 100 + dt.minute) * 100 + dt.second,
zd.microsecond // self.log_div, dt.microsecond // self.log_div,
) )
if c and not self.args.no_ansi: if c and not self.args.no_ansi:
@@ -985,41 +1157,26 @@ class SvcHub(object):
if not self.args.no_logflush: if not self.args.no_logflush:
self.logf.flush() self.logf.flush()
now = time.time() if dt.day != self.cday or dt.month != self.cmon:
if int(now) >= self.next_day: self._set_next_day(dt)
self._set_next_day()
def _set_next_day(self) -> None: def _set_next_day(self, dt: datetime) -> None:
if self.next_day and self.logf and self.logf_base_fn != self._logname(): if self.cday and self.logf and self.logf_base_fn != self._logname():
self.logf.close() self.logf.close()
self._setup_logfile("") self._setup_logfile("")
dt = datetime.now(UTC) self.cday = dt.day
self.cmon = dt.month
# 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)
def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
"""handles logging from all components""" """handles logging from all components"""
with self.log_mutex: with self.log_mutex:
now = time.time() dt = datetime.now(self.tz)
if int(now) >= self.next_day: if dt.day != self.cday or dt.month != self.cmon:
dt = datetime.fromtimestamp(now, UTC)
zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n" zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n"
zs = zs.format(dt.strftime("%Y-%m-%d")) zs = zs.format(dt.strftime("%Y-%m-%d"))
print(zs, end="") print(zs, end="")
self._set_next_day() self._set_next_day(dt)
if self.logf: if self.logf:
self.logf.write(zs) self.logf.write(zs)
@@ -1038,12 +1195,11 @@ class SvcHub(object):
else: else:
msg = "%s%s\033[0m" % (c, msg) msg = "%s%s\033[0m" % (c, msg)
zd = datetime.fromtimestamp(now, UTC)
ts = self.log_efmt % ( ts = self.log_efmt % (
zd.hour, dt.hour,
zd.minute, dt.minute,
zd.second, dt.second,
zd.microsecond // self.log_div, dt.microsecond // self.log_div,
) )
msg = fmt % (ts, src, msg) msg = fmt % (ts, src, msg)
try: try:
@@ -1141,5 +1297,5 @@ class SvcHub(object):
zs = "{}\n{}".format(VERSIONS, alltrace()) zs = "{}\n{}".format(VERSIONS, alltrace())
zb = zs.encode("utf-8", "replace") zb = zs.encode("utf-8", "replace")
zb = gzip.compress(zb) zb = gzip.compress(zb)
zs = base64.b64encode(zb).decode("ascii") zs = ub64enc(zb).decode("ascii")
self.log("stacks", zs) self.log("stacks", zs)

View File

@@ -226,10 +226,22 @@ class TcpSrv(object):
self.log("tcpsrv", msg, c) self.log("tcpsrv", msg, c)
def _listen(self, ip: str, port: int) -> None: def _listen(self, ip: str, port: int) -> None:
uds_perm = uds_gid = -1
if "unix:" in ip: if "unix:" in ip:
tcp = False tcp = False
ipv = socket.AF_UNIX ipv = socket.AF_UNIX
ip = ip.split("unix:")[1] uds = ip.split(":")
ip = uds[-1]
if len(uds) > 2:
uds_perm = int(uds[1], 8)
if len(uds) > 3:
try:
uds_gid = int(uds[2])
except:
import grp
uds_gid = grp.getgrnam(uds[2]).gr_gid
elif ":" in ip: elif ":" in ip:
tcp = True tcp = True
ipv = socket.AF_INET6 ipv = socket.AF_INET6
@@ -265,7 +277,13 @@ class TcpSrv(object):
srv.bind(ip) srv.bind(ip)
else: else:
tf = "%s.%d" % (ip, os.getpid()) tf = "%s.%d" % (ip, os.getpid())
if os.path.exists(tf):
os.unlink(tf)
srv.bind(tf) srv.bind(tf)
if uds_gid != -1:
os.chown(tf, -1, uds_gid)
if uds_perm != -1:
os.chmod(tf, uds_perm)
atomic_move(self.nlog, tf, ip, VF_CAREFUL) atomic_move(self.nlog, tf, ip, VF_CAREFUL)
sport = srv.getsockname()[1] if tcp else port sport = srv.getsockname()[1] if tcp else port

View File

@@ -403,7 +403,7 @@ class Tftpd(object):
bos.stat(ap) bos.stat(ap)
return True return True
except: except:
return False return vpath == "/"
def _p_isdir(self, vpath: str) -> bool: def _p_isdir(self, vpath: str) -> bool:
try: try:
@@ -411,7 +411,7 @@ class Tftpd(object):
ret = stat.S_ISDIR(st.st_mode) ret = stat.S_ISDIR(st.st_mode)
return ret return ret
except: except:
return False return vpath == "/"
def _hook(self, *a: Any, **ka: Any) -> None: def _hook(self, *a: Any, **ka: Any) -> None:
src = inspect.currentframe().f_back.f_code.co_name src = inspect.currentframe().f_back.f_code.co_name

View File

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

View File

@@ -59,6 +59,8 @@ class U2idx(object):
self.mem_cur = sqlite3.connect(":memory:", check_same_thread=False).cursor() self.mem_cur = sqlite3.connect(":memory:", check_same_thread=False).cursor()
self.mem_cur.execute(r"create table a (b text)") self.mem_cur.execute(r"create table a (b text)")
self.sh_cur: Optional["sqlite3.Cursor"] = None
self.p_end = 0.0 self.p_end = 0.0
self.p_dur = 0.0 self.p_dur = 0.0
@@ -95,17 +97,31 @@ class U2idx(object):
except: except:
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
def get_cur(self, vn: VFS) -> Optional["sqlite3.Cursor"]: def get_shr(self) -> Optional["sqlite3.Cursor"]:
if not HAVE_SQLITE3: if self.sh_cur:
return self.sh_cur
if not HAVE_SQLITE3 or not self.args.shr:
return None return None
assert sqlite3 # type: ignore # !rm
db = sqlite3.connect(self.args.shr_db, timeout=2, check_same_thread=False)
cur = db.cursor()
cur.execute('pragma table_info("sh")').fetchall()
self.sh_cur = cur
return cur
def get_cur(self, vn: VFS) -> Optional["sqlite3.Cursor"]:
cur = self.cur.get(vn.realpath) cur = self.cur.get(vn.realpath)
if cur: if cur:
return cur return cur
if "e2d" not in vn.flags: if not HAVE_SQLITE3 or "e2d" not in vn.flags:
return None return None
assert sqlite3 # type: ignore # !rm
ptop = vn.realpath ptop = vn.realpath
histpath = self.asrv.vfs.histtab.get(ptop) histpath = self.asrv.vfs.histtab.get(ptop)
if not histpath: if not histpath:
@@ -451,5 +467,5 @@ class U2idx(object):
return return
if identifier == self.active_id: if identifier == self.active_id:
assert self.active_cur assert self.active_cur # !rm
self.active_cur.connection.interrupt() self.active_cur.connection.interrupt()

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ from __future__ import print_function, unicode_literals
import argparse import argparse
import base64 import base64
import contextlib import binascii
import errno import errno
import hashlib import hashlib
import hmac import hmac
@@ -30,13 +30,10 @@ from collections import Counter
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
from queue import Queue 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
from .__version__ import S_BUILD_DT, S_VERSION from .__version__ import S_BUILD_DT, S_VERSION
from .stolen import surrogateescape from .stolen import surrogateescape
ub64dec = base64.urlsafe_b64decode
ub64enc = base64.urlsafe_b64encode
try: try:
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -64,7 +61,7 @@ if PY2:
if sys.version_info >= (3, 7) or ( 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 ODict = dict
else: else:
@@ -164,12 +161,8 @@ except ImportError:
if not PY2: if not PY2:
from io import BytesIO from io import BytesIO
from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote
else: else:
from StringIO import StringIO as BytesIO # type: ignore 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: try:
@@ -216,7 +209,7 @@ else:
FS_ENCODING = sys.getfilesystemencoding() 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' META_NOBOTS = '<meta name="robots" content="noindex, nofollow">\n'
@@ -260,6 +253,8 @@ IMPLICATIONS = [
["e2vu", "e2v"], ["e2vu", "e2v"],
["e2vp", "e2v"], ["e2vp", "e2v"],
["e2v", "e2d"], ["e2v", "e2d"],
["hardlink_only", "hardlink"],
["hardlink", "dedup"],
["tftpvv", "tftpv"], ["tftpvv", "tftpv"],
["smbw", "smb"], ["smbw", "smb"],
["smb1", "smb"], ["smb1", "smb"],
@@ -336,7 +331,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_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" DEF_MTH = ".vq,.aq,vc,ac,fmt,res,.fps"
@@ -438,7 +433,7 @@ def py_desc() -> str:
def _sqlite_ver() -> str: def _sqlite_ver() -> str:
assert sqlite3 # type: ignore assert sqlite3 # type: ignore # !rm
try: try:
co = sqlite3.connect(":memory:") co = sqlite3.connect(":memory:")
cur = co.cursor() cur = co.cursor()
@@ -486,17 +481,36 @@ VERSIONS = (
) )
_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER) try:
__all__ = [ _b64_enc_tl = bytes.maketrans(b"+/", b"-_")
"mp", _b64_dec_tl = bytes.maketrans(b"-_", b"+/")
"BytesIO",
"quote", def ub64enc(bs: bytes) -> bytes:
"unquote", x = binascii.b2a_base64(bs, newline=False)
"SQLITE_VER", return x.translate(_b64_enc_tl)
"JINJA_VER",
"PYFTPD_VER", def ub64dec(bs: bytes) -> bytes:
"PARTFTPY_VER", 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): class Daemon(threading.Thread):
@@ -915,7 +929,6 @@ class ProgressPrinter(threading.Thread):
self.msg = "" self.msg = ""
self.end = False self.end = False
self.n = -1 self.n = -1
self.start()
def run(self) -> None: def run(self) -> None:
sigblock() sigblock()
@@ -1029,7 +1042,7 @@ class MTHash(object):
if self.stop: if self.stop:
return nch, "", ofs0, chunk_sz return nch, "", ofs0, chunk_sz
assert f assert f # !rm
hashobj = hashlib.sha512() hashobj = hashlib.sha512()
while chunk_rem > 0: while chunk_rem > 0:
with self.imutex: with self.imutex:
@@ -1044,7 +1057,7 @@ class MTHash(object):
ofs += len(buf) ofs += len(buf)
bdig = hashobj.digest()[:33] bdig = hashobj.digest()[:33]
udig = base64.urlsafe_b64encode(bdig).decode("utf-8") udig = ub64enc(bdig).decode("ascii")
return nch, udig, ofs0, chunk_sz return nch, udig, ofs0, chunk_sz
@@ -1070,7 +1083,7 @@ class HMaccas(object):
self.cache = {} self.cache = {}
zb = hmac.new(self.key, msg, hashlib.sha512).digest() 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 self.cache[msg] = zs
return zs return zs
@@ -1401,18 +1414,13 @@ def min_ex(max_lines: int = 8, reverse: bool = False) -> str:
return "\n".join(ex[-max_lines:][:: -1 if reverse else 1]) return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
@contextlib.contextmanager def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str]:
def ren_open(
fname: str, *args: Any, **kwargs: Any
) -> Generator[dict[str, tuple[typing.IO[Any], str]], None, None]:
fun = kwargs.pop("fun", open) fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None) fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None) suffix = kwargs.pop("suffix", None)
if fname == os.devnull: if fname == os.devnull:
with fun(fname, *args, **kwargs) as f: return fun(fname, *args, **kwargs), fname
yield {"orz": (f, fname)}
return
if suffix: if suffix:
ext = fname.split(".")[-1] ext = fname.split(".")[-1]
@@ -1434,6 +1442,7 @@ def ren_open(
asciified = False asciified = False
b64 = "" b64 = ""
while True: while True:
f = None
try: try:
if fdir: if fdir:
fpath = os.path.join(fdir, fname) fpath = os.path.join(fdir, fname)
@@ -1445,19 +1454,20 @@ def ren_open(
fname += suffix fname += suffix
ext += suffix ext += suffix
with fun(fsenc(fpath), *args, **kwargs) as f: f = fun(fsenc(fpath), *args, **kwargs)
if b64: if b64:
assert fdir assert fdir # !rm
fp2 = "fn-trunc.%s.txt" % (b64,) fp2 = "fn-trunc.%s.txt" % (b64,)
fp2 = os.path.join(fdir, fp2) fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2: with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8")) f2.write(orig_name.encode("utf-8"))
yield {"orz": (f, fname)} return f, fname
return
except OSError as ex_: except OSError as ex_:
ex = ex_ ex = ex_
if f:
f.close()
# EPERM: android13 # EPERM: android13
if ex.errno in (errno.EINVAL, errno.EPERM) and not asciified: if ex.errno in (errno.EINVAL, errno.EPERM) and not asciified:
@@ -1478,8 +1488,7 @@ def ren_open(
if not b64: if not b64:
zs = ("%s\n%s" % (orig_name, suffix)).encode("utf-8", "replace") zs = ("%s\n%s" % (orig_name, suffix)).encode("utf-8", "replace")
zs = hashlib.sha512(zs).digest()[:12] b64 = ub64enc(hashlib.sha512(zs).digest()[:12]).decode("ascii")
b64 = base64.urlsafe_b64encode(zs).decode("utf-8")
badlen = len(fname) badlen = len(fname)
while len(fname) >= badlen: while len(fname) >= badlen:
@@ -1712,7 +1721,7 @@ class MultipartParser(object):
returns the value of the next field in the multipart body, returns the value of the next field in the multipart body,
raises if the field name is not as expected 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) p_field, p_fname, p_data = next(self.gen)
if p_field != field_name: if p_field != field_name:
raise WrongPostKey(field_name, p_field, p_fname, p_data) raise WrongPostKey(field_name, p_field, p_fname, p_data)
@@ -1721,7 +1730,7 @@ class MultipartParser(object):
def drop(self) -> None: def drop(self) -> None:
"""discards the remaining multipart body""" """discards the remaining multipart body"""
assert self.gen assert self.gen # !rm
for _, _, data in self.gen: for _, _, data in self.gen:
for _ in data: for _ in data:
pass pass
@@ -1760,7 +1769,7 @@ def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
ofs = ret.find(b"\r\n\r\n") ofs = ret.find(b"\r\n\r\n")
if ofs < 0: if ofs < 0:
if len(ret) > 1024 * 64: if len(ret) > 1024 * 32:
raise Pebkac(400, "header 2big") raise Pebkac(400, "header 2big")
else: else:
continue continue
@@ -1785,9 +1794,8 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
nc = rnd + extra nc = rnd + extra
nb = (6 + 6 * nc) // 8 nb = (6 + 6 * nc) // 8
zb = os.urandom(nb) zb = ub64enc(os.urandom(nb))
zb = base64.urlsafe_b64encode(zb) fn = zb[:nc].decode("ascii") + ext
fn = zb[:nc].decode("utf-8") + ext
ok = not os.path.exists(fsenc(os.path.join(fdir, fn))) ok = not os.path.exists(fsenc(os.path.join(fdir, fn)))
return fn return fn
@@ -1800,7 +1808,7 @@ def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str
zs = "%s %s" % (salt, fspath) zs = "%s %s" % (salt, fspath)
zb = zs.encode("utf-8", "replace") 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( def gen_filekey_dbg(
@@ -1814,7 +1822,7 @@ def gen_filekey_dbg(
) -> str: ) -> str:
ret = gen_filekey(alg, salt, fspath, fsize, inode) ret = gen_filekey(alg, salt, fspath, fsize, inode)
assert log_ptn assert log_ptn # !rm
if log_ptn.search(fspath): if log_ptn.search(fspath):
try: try:
import inspect import inspect
@@ -2073,6 +2081,8 @@ def html_bescape(s: bytes, quot: bool = False, crlf: bool = False) -> bytes:
def _quotep2(txt: str) -> str: def _quotep2(txt: str) -> str:
"""url quoter which deals with bytes correctly""" """url quoter which deals with bytes correctly"""
if not txt:
return ""
btxt = w8enc(txt) btxt = w8enc(txt)
quot = quote(btxt, safe=b"/") quot = quote(btxt, safe=b"/")
return w8dec(quot.replace(b" ", b"+")) # type: ignore return w8dec(quot.replace(b" ", b"+")) # type: ignore
@@ -2080,18 +2090,61 @@ def _quotep2(txt: str) -> str:
def _quotep3(txt: str) -> str: def _quotep3(txt: str) -> str:
"""url quoter which deals with bytes correctly""" """url quoter which deals with bytes correctly"""
if not txt:
return ""
btxt = w8enc(txt) btxt = w8enc(txt)
quot = quote(btxt, safe=b"/").encode("utf-8") quot = quote(btxt, safe=b"/").encode("utf-8")
return w8dec(quot.replace(b" ", b"+")) 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: def unquotep(txt: str) -> str:
"""url unquoter which deals with bytes correctly""" """url unquoter which deals with bytes correctly"""
btxt = w8enc(txt) btxt = w8enc(txt)
# btxt = btxt.replace(b"+", b" ")
unq2 = unquote(btxt) unq2 = unquote(btxt)
return w8dec(unq2) return w8dec(unq2)
@@ -2237,12 +2290,12 @@ w8enc = _w8enc3 if not PY2 else _w8enc2
def w8b64dec(txt: str) -> str: def w8b64dec(txt: str) -> str:
"""decodes base64(filesystem-bytes) to wtf8""" """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: def w8b64enc(txt: str) -> str:
"""encodes wtf8 to base64(filesystem-bytes)""" """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: if not PY2 and WINDOWS:
@@ -2400,7 +2453,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
def get_df(abspath: str) -> tuple[Optional[int], Optional[int]]: def get_df(abspath: str) -> tuple[Optional[int], Optional[int]]:
try: try:
# some fuses misbehave # some fuses misbehave
assert ctypes assert ctypes # type: ignore # !rm
if ANYWIN: if ANYWIN:
bfree = ctypes.c_ulonglong(0) bfree = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
@@ -2618,8 +2671,7 @@ def hashcopy(
if slp: if slp:
time.sleep(slp) time.sleep(slp)
digest = hashobj.digest()[:33] digest_b64 = ub64enc(hashobj.digest()[:33]).decode("ascii")
digest_b64 = base64.urlsafe_b64encode(digest).decode("utf-8")
return tlen, hashobj.hexdigest(), digest_b64 return tlen, hashobj.hexdigest(), digest_b64
@@ -2859,7 +2911,7 @@ def getalive(pids: list[int], pgid: int) -> list[int]:
alive.append(pid) alive.append(pid)
else: else:
# windows doesn't have pgroups; assume # windows doesn't have pgroups; assume
assert psutil assert psutil # type: ignore # !rm
psutil.Process(pid) psutil.Process(pid)
alive.append(pid) alive.append(pid)
except: except:
@@ -2877,7 +2929,7 @@ def killtree(root: int) -> None:
pgid = 0 pgid = 0
if HAVE_PSUTIL: if HAVE_PSUTIL:
assert psutil assert psutil # type: ignore # !rm
pids = [root] pids = [root]
parent = psutil.Process(root) parent = psutil.Process(root)
for child in parent.children(recursive=True): for child in parent.children(recursive=True):
@@ -3290,7 +3342,7 @@ def runhook(
at: float, at: float,
txt: str, txt: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
assert broker or up2k assert broker or up2k # !rm
asrv = (broker or up2k).asrv asrv = (broker or up2k).asrv
args = (broker or up2k).args args = (broker or up2k).args
vp = vp.replace("\\", "/") vp = vp.replace("\\", "/")
@@ -3484,7 +3536,7 @@ def termsize() -> tuple[int, int]:
def hidedir(dp) -> None: def hidedir(dp) -> None:
if ANYWIN: if ANYWIN:
try: try:
assert ctypes assert ctypes # type: ignore # !rm
k32 = ctypes.WinDLL("kernel32") k32 = ctypes.WinDLL("kernel32")
attrs = k32.GetFileAttributesW(dp) attrs = k32.GetFileAttributesW(dp)
if attrs >= 0: if attrs >= 0:
@@ -3520,3 +3572,16 @@ class WrongPostKey(Pebkac):
self.got = got self.got = got
self.fname = fname self.fname = fname
self.datagen = datagen 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

@@ -10,7 +10,6 @@
--fg2-max: #fff; --fg2-max: #fff;
--fg-weak: #bbb; --fg-weak: #bbb;
--bg-u7: #555;
--bg-u6: #4c4c4c; --bg-u6: #4c4c4c;
--bg-u5: #444; --bg-u5: #444;
--bg-u4: #383838; --bg-u4: #383838;
@@ -43,8 +42,14 @@
--btn-h-bg: #805; --btn-h-bg: #805;
--btn-1-fg: #400; --btn-1-fg: #400;
--btn-1-bg: var(--a); --btn-1-bg: var(--a);
--btn-h-bs: var(--btn-bs);
--btn-h-bb: var(--btn-bb);
--btn-1-bs: var(--btn-bs);
--btn-1-bb: var(--btn-bb);
--btn-1h-fg: var(--btn-1-fg); --btn-1h-fg: var(--btn-1-fg);
--btn-1h-bg: #fe8; --btn-1h-bg: #fe8;
--btn-1h-bs: var(--btn-1-bs);
--btn-1h-bb: var(--btn-1-bb);
--chk-fg: var(--tab-alt); --chk-fg: var(--tab-alt);
--txt-sh: var(--bg-d2); --txt-sh: var(--bg-d2);
--txt-bg: var(--btn-bg); --txt-bg: var(--btn-bg);
@@ -59,7 +64,7 @@
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1)); --u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-b1: rgba(128,128,128,0.8); --u2-tab-b1: rgba(128,128,128,0.8);
--u2-tab-1-fg: #fd7; --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-b1: #7c5;
--u2-tab-1-b2: #583; --u2-tab-1-b2: #583;
--u2-tab-1-sh: #280; --u2-tab-1-sh: #280;
@@ -212,22 +217,19 @@ html.y {
html.a { html.a {
--op-aa-sh: 0 0 .2em var(--bg-d3) inset; --op-aa-sh: 0 0 .2em var(--bg-d3) inset;
--u2-o-bg: #603; --btn-bs: 0 0 .2em var(--bg-d3);
--u2-o-b1: #a16; }
--u2-o-sh: #a00; html.az {
--u2-o-h-bg: var(--u2-o-bg); --btn-1-bs: 0 0 .1em var(--fg) inset;
--u2-o-h-b1: #fb0;
--u2-o-h-sh: #fb0;
--u2-o-1-bg: #6a1;
--u2-o-1-b1: #efa;
--u2-o-1-sh: #0c0;
--u2-o-1h-bg: var(--u2-o-1-bg);
} }
html.ay { html.ay {
--op-aa-sh: 0 .1em .2em #ccc; --op-aa-sh: 0 .1em .2em #ccc;
--op-aa-bg: var(--bg-max); --op-aa-bg: var(--bg-max);
} }
html.b { html.b {
--btn-bs: 0 .05em 0 var(--bg-d3) inset;
--btn-1-bs: 0 .05em 0 var(--btn-1h-bg) inset;
--tree-bg: var(--bg); --tree-bg: var(--bg);
--g-bg: var(--bg); --g-bg: var(--bg);
@@ -244,17 +246,13 @@ html.b {
--u2-b1-bg: rgba(128,128,128,0.15); --u2-b1-bg: rgba(128,128,128,0.15);
--u2-b2-bg: var(--u2-b1-bg); --u2-b2-bg: var(--u2-b1-bg);
--u2-o-bg: var(--btn-bg);
--u2-o-h-bg: var(--btn-h-bg);
--u2-o-1-bg: var(--a);
--u2-o-1h-bg: var(--a-hil);
--f-sh1: 0.1; --f-sh1: 0.1;
--mp-b-bg: transparent; --mp-b-bg: transparent;
} }
html.bz { html.bz {
--fg: #cce; --fg: #cce;
--fg-weak: #bbd; --fg-weak: #bbd;
--bg-u5: #3b3f58; --bg-u5: #3b3f58;
--bg-u4: #1e2130; --bg-u4: #1e2130;
--bg-u3: #1e2130; --bg-u3: #1e2130;
@@ -266,12 +264,14 @@ html.bz {
--row-alt: #181a27; --row-alt: #181a27;
--a-b: #fb4;
--btn-bg: #202231; --btn-bg: #202231;
--btn-h-bg: #2d2f45; --btn-h-bg: #2d2f45;
--btn-1-bg: #ba2959; --btn-1-bg: #eb6;
--btn-1-is: #f59; --btn-1-fg: #000;
--btn-1-fg: #fff;
--btn-1h-fg: #000; --btn-1h-fg: #000;
--btn-1h-bg: #ff9;
--txt-sh: a; --txt-sh: a;
--u2-tab-b1: var(--bg-u5); --u2-tab-b1: var(--bg-u5);
@@ -306,6 +306,7 @@ html.by {
} }
html.c { html.c {
font-weight: bold; font-weight: bold;
--fg: #fff; --fg: #fff;
--fg-weak: #cef; --fg-weak: #cef;
--bg-u5: #409; --bg-u5: #409;
@@ -326,17 +327,25 @@ html.c {
--chk-fg: #d90; --chk-fg: #d90;
--op-aa-bg: #f9dd22; --op-aa-bg: #f9dd22;
--u2-o-1-bg: #4cf;
--srv-1: #ea0; --srv-1: #ea0;
--mp-b-bg: transparent; --mp-b-bg: transparent;
} }
html.cz { html.cz {
--bgg: var(--bg-u2); --bgg: var(--bg-u2);
--sel-bg: var(--bg-u5); --sel-bg: var(--bg-u5);
--sel-fg: var(--fg); --sel-fg: var(--fg);
--btn-bb: .2em solid #709;
--btn-bs: 0 .1em .6em rgba(255,0,185,0.5);
--btn-1-bb: .2em solid #e90;
--btn-1-bs: 0 .1em .8em rgba(255,205,0,0.9);
--srv-3: #fff; --srv-3: #fff;
--u2-tab-b1: var(--bg-d3); --u2-tab-b1: var(--bg-d3);
--u2-tab-1-bg: a;
} }
html.cy { html.cy {
--fg: #fff; --fg: #fff;
@@ -363,16 +372,20 @@ html.cy {
--btn-h-fg: #fff; --btn-h-fg: #fff;
--btn-1-bg: #ff0; --btn-1-bg: #ff0;
--btn-1-fg: #000; --btn-1-fg: #000;
--btn-bs: 0 .25em 0 #f00;
--chk-fg: #fd0; --chk-fg: #fd0;
--txt-bg: #000;
--srv-1: #f00; --srv-1: #f00;
--srv-3: #fff; --srv-3: #fff;
--op-aa-bg: #fff; --op-aa-bg: #fff;
--u2-b1-bg: #f00; --u2-b1-bg: #f00;
--u2-b2-bg: #f00; --u2-b2-bg: #f00;
--u2-o-bg: #ff0;
--u2-o-1-bg: #f00; --g-sel-fg: #fff;
--g-sel-bg: #aaa;
--g-fsel-bg: #aaa;
} }
html.dz { html.dz {
--fg: #4d4; --fg: #4d4;
@@ -380,7 +393,6 @@ html.dz {
--fg2-max: #fff; --fg2-max: #fff;
--fg-weak: #2a2; --fg-weak: #2a2;
--bg-u7: #020;
--bg-u6: #020; --bg-u6: #020;
--bg-u5: #050; --bg-u5: #050;
--bg-u4: #020; --bg-u4: #020;
@@ -413,6 +425,9 @@ html.dz {
--btn-1-bg: #4f4; --btn-1-bg: #4f4;
--btn-1h-fg: var(--btn-1-fg); --btn-1h-fg: var(--btn-1-fg);
--btn-1h-bg: #3f3; --btn-1h-bg: #3f3;
--btn-bs: 0 0 0 .1em #080 inset;
--btn-1-bs: a;
--chk-fg: var(--tab-alt); --chk-fg: var(--tab-alt);
--txt-sh: var(--bg-d2); --txt-sh: var(--bg-d2);
--txt-bg: var(--btn-bg); --txt-bg: var(--btn-bg);
@@ -427,19 +442,13 @@ html.dz {
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1)); --u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-b1: var(--fg-weak); --u2-tab-b1: var(--fg-weak);
--u2-tab-1-fg: #fff; --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-b1: #7c5;
--u2-tab-1-b2: #583; --u2-tab-1-b2: #583;
--u2-tab-1-sh: #280; --u2-tab-1-sh: #280;
--u2-b-fg: #fff; --u2-b-fg: #fff;
--u2-b1-bg: #3a3; --u2-b1-bg: #3a3;
--u2-b2-bg: #3a3; --u2-b2-bg: #3a3;
--u2-o-bg: var(--btn-bg);
--u2-o-b1: var(--bg-u5);
--u2-o-h-bg: var(--fg-weak);
--u2-o-1-bg: var(--fg-weak);
--u2-o-1-b1: var(--a);
--u2-o-1h-bg: var(--a);
--u2-inf-bg: #07a; --u2-inf-bg: #07a;
--u2-inf-b1: #0be; --u2-inf-b1: #0be;
--u2-ok-bg: #380; --u2-ok-bg: #380;
@@ -551,10 +560,6 @@ html.dy {
--u2-tab-1-bg: a; --u2-tab-1-bg: a;
--u2-b1-bg: #000; --u2-b1-bg: #000;
--u2-b2-bg: #000; --u2-b2-bg: #000;
--u2-o-h-bg: #999;
--u2-o-1h-bg: #999;
--u2-o-bg: #eee;
--u2-o-1-bg: #000;
--ud-b1: a; --ud-b1: a;
@@ -599,7 +604,7 @@ html.dy {
background: var(--sel-bg); background: var(--sel-bg);
text-shadow: none; text-shadow: none;
} }
html,body,tr,th,td,#files,a { html,body,tr,th,td,#files,a,#blogout {
color: inherit; color: inherit;
background: none; background: none;
font-weight: inherit; font-weight: inherit;
@@ -627,6 +632,7 @@ pre, code, tt, #doc, #doc>code {
overflow: hidden; overflow: hidden;
width: 0; width: 0;
height: 0; height: 0;
color: var(--bg);
} }
html .ayjump:focus { html .ayjump:focus {
z-index: 80386; z-index: 80386;
@@ -681,11 +687,15 @@ html.y #path {
#files tbody div a { #files tbody div a {
color: var(--tab-alt); color: var(--tab-alt);
} }
a, #files tbody div a:last-child { a, #blogout, #files tbody div a:last-child {
color: var(--a); color: var(--a);
padding: .2em; padding: .2em;
text-decoration: none; text-decoration: none;
} }
#blogout {
margin: -.2em;
}
#blogout:hover,
a:hover { a:hover {
color: var(--a-hil); color: var(--a-hil);
background: var(--a-h-bg); background: var(--a-h-bg);
@@ -929,6 +939,9 @@ html.y #path a:hover {
color: var(--srv-3); color: var(--srv-3);
border-bottom: 1px solid var(--srv-3b); border-bottom: 1px solid var(--srv-3b);
} }
#flogout {
display: inline;
}
#goh+span { #goh+span {
color: var(--bg-u5); color: var(--bg-u5);
padding-left: .5em; padding-left: .5em;
@@ -963,6 +976,8 @@ html.y #path a:hover {
#files tbody tr.play a:hover { #files tbody tr.play a:hover {
color: var(--btn-1h-fg); color: var(--btn-1h-fg);
background: var(--btn-1h-bg); background: var(--btn-1h-bg);
box-shadow: var(--btn-1h-bs);
border-bottom: var(--btn-1h-bb);
} }
#ggrid { #ggrid {
margin: -.2em -.5em; margin: -.2em -.5em;
@@ -971,6 +986,7 @@ html.y #path a:hover {
overflow: hidden; overflow: hidden;
display: block; display: block;
display: -webkit-box; display: -webkit-box;
line-clamp: var(--grid-ln);
-webkit-line-clamp: var(--grid-ln); -webkit-line-clamp: var(--grid-ln);
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
padding-top: .3em; padding-top: .3em;
@@ -1145,6 +1161,7 @@ html.y #widget.open {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#fshr,
#wtgrid, #wtgrid,
#wtico { #wtico {
position: relative; position: relative;
@@ -1331,6 +1348,7 @@ html.y #widget.open {
#widget.cmp #wtoggle { #widget.cmp #wtoggle {
font-size: 1.2em; font-size: 1.2em;
} }
#widget.cmp #fshr,
#widget.cmp #wtgrid { #widget.cmp #wtgrid {
display: none; display: none;
} }
@@ -1431,7 +1449,11 @@ input[type="checkbox"]+label {
input[type="radio"]:checked+label, input[type="radio"]:checked+label,
input[type="checkbox"]:checked+label { input[type="checkbox"]:checked+label {
color: #0e0; color: #0e0;
color: var(--a); color: var(--btn-1-bg);
}
input[type="checkbox"]:checked+label {
box-shadow: var(--btn-1-bs);
border-bottom: var(--btn-1-bb);
} }
html.dz input { html.dz input {
font-family: 'scp', monospace, monospace; font-family: 'scp', monospace, monospace;
@@ -1609,6 +1631,8 @@ html {
color: var(--btn-fg); color: var(--btn-fg);
background: #eee; background: #eee;
background: var(--btn-bg); background: var(--btn-bg);
box-shadow: var(--btn-bs);
border-bottom: var(--btn-bb);
border-radius: .3em; border-radius: .3em;
padding: .2em .4em; padding: .2em .4em;
font-size: 1.2em; font-size: 1.2em;
@@ -1622,20 +1646,14 @@ html.c .btn,
html.a .btn { html.a .btn {
border-radius: .2em; border-radius: .2em;
} }
html.cz .btn {
box-shadow: 0 .1em .6em rgba(255,0,185,0.5);
border-bottom: .2em solid #709;
}
html.dz .btn { html.dz .btn {
font-size: 1em; font-size: 1em;
box-shadow: 0 0 0 .1em #080 inset;
}
html.dz .tgl.btn.on {
box-shadow: 0 0 0 .1em var(--btn-1-bg) inset;
} }
.btn:hover { .btn:hover {
color: var(--btn-h-fg); color: var(--btn-h-fg);
background: var(--btn-h-bg); background: var(--btn-h-bg);
box-shadow: var(--btn-h-bs);
border-bottom: var(--btn-h-bb);
} }
.tgl.btn.on { .tgl.btn.on {
background: #000; background: #000;
@@ -1643,14 +1661,14 @@ html.dz .tgl.btn.on {
color: #fff; color: #fff;
color: var(--btn-1-fg); color: var(--btn-1-fg);
text-shadow: none; text-shadow: none;
} box-shadow: var(--btn-1-bs);
html.cz .tgl.btn.on { border-bottom: var(--btn-1-bb);
box-shadow: 0 .1em .8em rgba(255,205,0,0.9);
border-bottom: .2em solid #e90;
} }
.tgl.btn.on:hover { .tgl.btn.on:hover {
background: var(--btn-1h-bg);
color: var(--btn-1h-fg); color: var(--btn-1h-fg);
background: var(--btn-1h-bg);
box-shadow: var(--btn-1h-bs);
border-bottom: var(--btn-1h-bb);
} }
#detree { #detree {
padding: .3em .5em; padding: .3em .5em;
@@ -1855,6 +1873,7 @@ html.y #tree.nowrap .ntree a+a:hover {
#unpost td:nth-child(4) { #unpost td:nth-child(4) {
text-align: right; text-align: right;
} }
#shui,
#rui { #rui {
background: #fff; background: #fff;
background: var(--bg); background: var(--bg);
@@ -1870,13 +1889,25 @@ html.y #tree.nowrap .ntree a+a:hover {
padding: 1em; padding: 1em;
z-index: 765; z-index: 765;
} }
#shui div+div,
#rui div+div { #rui div+div {
margin-top: 1em; margin-top: 1em;
} }
#shui table,
#rui table { #rui table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
} }
#shui button {
margin: 0 1em 0 0;
}
#shui .btn {
font-size: 1em;
}
#shui td {
padding: .8em 0;
}
#shui td+td,
#rui td+td { #rui td+td {
padding: .2em 0 .2em .5em; padding: .2em 0 .2em .5em;
} }
@@ -1884,10 +1915,15 @@ html.y #tree.nowrap .ntree a+a:hover {
font-family: 'scp', monospace, monospace; font-family: 'scp', monospace, monospace;
font-family: var(--font-mono), 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace;
} }
#shui td+td,
#rui td+td, #rui td+td,
#shui td input[type="text"],
#rui td input[type="text"] { #rui td input[type="text"] {
width: 100%; width: 100%;
} }
#shui td.exs input[type="text"] {
width: 3em;
}
#rn_f.m td:first-child { #rn_f.m td:first-child {
white-space: nowrap; white-space: nowrap;
} }
@@ -2682,23 +2718,25 @@ html.b #u2conf a.b:hover {
#u2conf input[type="checkbox"]:checked+label { #u2conf input[type="checkbox"]:checked+label {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
background: var(--u2-o-bg); background: var(--btn-bg);
border-bottom: .2em solid var(--u2-o-b1); box-shadow: var(--btn-bs);
box-shadow: 0 .1em .3em var(--u2-o-sh) inset; border-bottom: var(--btn-bb);
text-shadow: 1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000; text-shadow: 1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000;
} }
#u2conf input[type="checkbox"]:checked+label { #u2conf input[type="checkbox"]:checked+label {
background: var(--u2-o-1-bg); background: var(--btn-1-bg);
border-bottom: .2em solid var(--u2-o-1-b1); box-shadow: var(--btn-1-bs);
box-shadow: 0 .1em .5em var(--u2-o-1-sh); border-bottom: var(--btn-1-bb);
} }
#u2conf input[type="checkbox"]+label:hover { #u2conf input[type="checkbox"]+label:hover {
box-shadow: 0 .1em .3em var(--u2-o-h-sh); background: var(--btn-h-bg);
border-color: var(--u2-o-h-b1); box-shadow: var(--btn-h-bs);
background: var(--u2-o-h-bg); border-bottom: var(--btn-h-bb);
} }
#u2conf input[type="checkbox"]:checked+label:hover { #u2conf input[type="checkbox"]:checked+label:hover {
background: var(--u2-o-1h-bg); background: var(--btn-1h-bg);
box-shadow: var(--btn-1h-bs);
border-bottom: var(--btn-1h-bb);
} }
#op_up2k.srch #u2conf td:nth-child(2)>*, #op_up2k.srch #u2conf td:nth-child(2)>*,
#op_up2k.srch #u2conf td:nth-child(3)>* { #op_up2k.srch #u2conf td:nth-child(3)>* {
@@ -3058,14 +3096,6 @@ html.b #ggrid>a {
html.b .btn { html.b .btn {
top: -.1em; top: -.1em;
} }
html.b .btn,
html.b #u2conf a.b,
html.b #u2conf input[type="checkbox"]:not(:checked)+label {
box-shadow: 0 .05em 0 var(--bg-d3) inset;
}
html.b .tgl.btn.on {
box-shadow: 0 .05em 0 var(--btn-1-is) inset;
}
html.b #op_up2k.srch sup { html.b #op_up2k.srch sup {
color: #fc0; color: #fc0;
} }
@@ -3095,18 +3125,30 @@ html.by #u2cards a.act {
html.cy #wrap {
color: #000;
}
html.cy .mdo a { html.cy .mdo a {
background: #f00; background: #f00;
} }
html.cy #wrap,
html.cy #acc_info a,
html.cy #op_up2k, html.cy #op_up2k,
html.cy #files, html.cy #files,
html.cy #files a, html.cy #files a,
html.cy #files tbody div a:last-child { html.cy #files tbody div a:last-child {
color: #000; 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);
}

File diff suppressed because it is too large Load Diff

82
copyparty/web/shares.css Normal file
View File

@@ -0,0 +1,82 @@
html {
color: #333;
background: #f7f7f7;
font-family: sans-serif;
font-family: var(--font-main), sans-serif;
touch-action: manipulation;
}
#wrap {
margin: 2em auto;
padding: 0 1em 3em 1em;
line-height: 2.3em;
}
#wrap>span {
margin: 0 0 0 1em;
border-bottom: 1px solid #999;
}
li {
margin: 1em 0;
}
a {
color: #047;
background: #fff;
text-decoration: none;
white-space: nowrap;
border-bottom: 1px solid #8ab;
border-radius: .2em;
padding: .2em .6em;
margin: 0 .3em;
}
td a {
margin: 0;
}
#w {
color: #fff;
background: #940;
border-color: #b70;
}
#repl {
border: none;
background: none;
color: inherit;
padding: 0;
position: fixed;
bottom: .25em;
left: .2em;
}
table {
border-collapse: collapse;
position: relative;
}
th {
top: -1px;
position: sticky;
background: #f7f7f7;
}
td, th {
padding: .3em .6em;
text-align: left;
white-space: nowrap;
}
td+td+td+td+td+td+td+td {
font-family: var(--font-mono), monospace, monospace;
}
html.z {
background: #222;
color: #ccc;
}
html.z a {
color: #fff;
background: #057;
border-color: #37a;
}
html.z th {
background: #222;
}
html.bz {
color: #bbd;
background: #11121d;
}

76
copyparty/web/shares.html Normal file
View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ s_doctitle }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
</head>
<body>
<div id="wrap">
<a id="a" href="{{ r }}/?shares" class="af">refresh</a>
<a id="a" href="{{ r }}/?h" class="af">control-panel</a>
<span>axs = perms (read,write,move,delet)</span>
<span>nf = numFiles (0=dir)</span>
<span>min/hrs = time left</span>
<table id="tab"><thead><tr>
<th>delete</th>
<th>sharekey</th>
<th>pw</th>
<th>source</th>
<th>axs</th>
<th>nf</th>
<th>user</th>
<th>created</th>
<th>expires</th>
<th>min</th>
<th>hrs</th>
<th>add time</th>
</tr></thead><tbody>
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
<tr>
<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>{{ pr }}</td>
<td>{{ st }}</td>
<td>{{ un|e }}</td>
<td>{{ t0 }}</td>
<td>{{ t1 }}</td>
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 60) | round(1) }}</td>
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
<td></td>
</tr>
{% endfor %}
</tbody></table>
{% if not rows %}
(you don't have any active shares btw)
{% endif %}
<script>
var SR = {{ r|tojson }},
shr="{{ shr }}",
lang="{{ lang }}",
dfavico="{{ favico }}";
var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
</script>
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/shares.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
{%- endif %}
</body>
</html>

56
copyparty/web/shares.js Normal file
View File

@@ -0,0 +1,56 @@
var t = QSA('a[k]');
for (var a = 0; a < t.length; a++)
t[a].onclick = rm;
function rm() {
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?eshare=rm',
xhr = new XHR();
xhr.open('POST', u, true);
xhr.onload = xhr.onerror = cb;
xhr.send();
}
function bump() {
var k = this.closest('tr').getElementsByTagName('a')[0].getAttribute('k'),
u = SR + shr + uricom_enc(k) + '?eshare=' + this.value,
xhr = new XHR();
xhr.open('POST', u, true);
xhr.onload = xhr.onerror = cb;
xhr.send();
}
function cb() {
if (this.status !== 200)
return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText)));
document.location = '?shares';
}
(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 b = 7; b < 9; b++)
buf.push(parseInt(tr[a].cells[b].innerHTML));
var ibuf = 0;
for (var a = 0; a < tr.length; a++)
for (var b = 7; b < 9; b++) {
var v = buf[ibuf++];
tr[a].cells[b].innerHTML =
v ? unix2iso(v).replace(' ', ',&nbsp;') : 'never';
}
for (var a = 0; a < tr.length; a++)
tr[a].cells[11].innerHTML =
'<button value="1">1min</button> ' +
'<button value="60">1h</button>';
var btns = QSA('td button'), aa = btns.length;
for (var a = 0; a < aa; a++)
btns[a].onclick = bump;
})();

View File

@@ -53,7 +53,7 @@ a.r {
border-color: #c7a; border-color: #c7a;
} }
a.g { a.g {
color: #2b0; color: #0a0;
border-color: #3a0; border-color: #3a0;
box-shadow: 0 .3em 1em #4c0; box-shadow: 0 .3em 1em #4c0;
} }
@@ -152,11 +152,13 @@ pre b,
code b { code b {
color: #000; color: #000;
font-weight: normal; 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 pre b,
html.z code b { html.z code b {
color: #fff; color: #fff;
border-bottom: 1px solid #9f9;
} }
@@ -182,13 +184,18 @@ html.z a.g {
border-color: #af4; border-color: #af4;
box-shadow: 0 .3em 1em #7d0; box-shadow: 0 .3em 1em #7d0;
} }
form {
line-height: 2.5em;
}
#x,
input { input {
color: #a50; color: #a50;
background: #fff; background: #fff;
border: 1px solid #a50; border: 1px solid #a50;
border-radius: .5em; border-radius: .3em;
padding: .5em .7em; padding: .25em .6em;
margin: 0 .5em 0 0; margin: 0 .3em 0 0;
font-size: 1em;
} }
input::placeholder { input::placeholder {
font-size: 1.2em; font-size: 1.2em;
@@ -197,6 +204,7 @@ input::placeholder {
opacity: 0.64; opacity: 0.64;
color: #930; color: #930;
} }
#x,
html.z input { html.z input {
color: #fff; color: #fff;
background: #626; background: #626;

View File

@@ -14,6 +14,7 @@
<body> <body>
<div id="wrap"> <div id="wrap">
{%- if not in_shr %}
<a id="a" href="{{ r }}/?h" class="af">refresh</a> <a id="a" href="{{ r }}/?h" class="af">refresh</a>
<a id="v" href="{{ r }}/?hc" class="af">connect</a> <a id="v" href="{{ r }}/?hc" class="af">connect</a>
@@ -21,7 +22,8 @@
<p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p> <p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
{%- else %} {%- else %}
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
<p><span id="m">welcome back,</span> <strong>{{ this.uname }}</strong></p> <p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p>
{%- endif %}
{%- endif %} {%- endif %}
{%- if msg %} {%- if msg %}
@@ -58,6 +60,18 @@
</div> </div>
{%- endif %} {%- 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 rvol %} {%- if rvol %}
<h1 id="f">you can browse:</h1> <h1 id="f">you can browse:</h1>
<ul> <ul>
@@ -76,8 +90,43 @@
</ul> </ul>
{%- endif %} {%- endif %}
<h1 id="cc">client config:</h1> {%- if in_shr %}
<h1 id="z">unlock this share:</h1>
<div>
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" id="la" name="act" value="login" />
<input type="password" id="lp" name="cppwd" placeholder=" password" />
<input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="Unlock" />
{% if ahttps %}
<a id="w" href="{{ ahttps }}">switch to https</a>
{% endif %}
</form>
</div>
{%- else %}
<h1 id="l">login for more:</h1>
<div>
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" id="la" name="act" value="login" />
<input type="password" id="lp" name="cppwd" placeholder=" password" />
<input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="Login" />
{% if chpw %}
<a id="x" href="#">change password</a>
{% endif %}
{% if ahttps %}
<a id="w" href="{{ ahttps }}">switch to https</a>
{% endif %}
</form>
</div>
{%- endif %}
<h1 id="cc">other stuff:</h1>
<ul> <ul>
{%- if this.uname != '*' and this.args.shr %}
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
{% endif %}
{% if k304 or k304vis %} {% if k304 or k304vis %}
{% if k304 %} {% if k304 %}
<li><a id="h" href="{{ r }}/?k304=n">disable k304</a> (currently enabled) <li><a id="h" href="{{ r }}/?k304=n">disable k304</a> (currently enabled)
@@ -90,18 +139,6 @@
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li> <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
</ul> </ul>
<h1 id="l">login for more:</h1>
<div>
<form method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" name="act" value="login" />
<input type="password" name="cppwd" placeholder=" password" />
<input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" value="Login" />
{% if ahttps %}
<a id="w" href="{{ ahttps }}">switch to https</a>
{% endif %}
</form>
</div>
</div> </div>
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
{%- if not this.args.nb %} {%- if not this.args.nb %}

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", "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:", "f1": "du kan betrakte:",
"g1": "du kan laste opp til:", "g1": "du kan laste opp til:",
"cc1": "klient-konfigurasjon", "cc1": "brytere og sånt",
"h1": "skru av k304", "h1": "skru av k304",
"i1": "skru på 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", "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",
@@ -17,9 +17,9 @@ var Ls = {
"l1": "logg inn:", "l1": "logg inn:",
"m1": "velkommen tilbake,", "m1": "velkommen tilbake,",
"n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌", "n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikke har tilgang? prøv å logge inn eller <a href="' + SR + '/?h">gå hjem</a>', "o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>',
"p1": "403: tilgang nektet &nbsp;~┻━┻", "p1": "403: tilgang nektet &nbsp;~┻━┻",
"q1": 'du må logge inn eller <a href="' + SR + '/?h">gå hjem</a>', "q1": 'prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>',
"r1": "gå hjem", "r1": "gå hjem",
".s1": "kartlegg", ".s1": "kartlegg",
"t1": "handling", "t1": "handling",
@@ -27,21 +27,67 @@ var Ls = {
"v1": "koble til", "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$N$NADVARSEL: kommer til å vise passordet ditt!",
"w1": "bytt til https", "w1": "bytt til https",
"x1": "bytt passord",
"y1": "dine delinger",
"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": { "eng": {
"d2": "shows the state of all active threads", "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", "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", "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$N$NWARNING: this will show your password!",
"ta1": "fill in your new password first",
"ta2": "repeat to confirm new password:",
"ta3": "found a typo; please try again",
},
"chi": {
"a1": "更新",
"b1": "你好 &nbsp; <small>(你尚未登录)</small>",
"c1": "登出",
"d1": "状态",
"d2": "显示所有活动线程的状态",
"e1": "重新加载配置",
"e2": "重新加载配置文件(账户/卷/卷标),$N并重新扫描所有 e2ds 卷$N$N注意任何全局设置的更改$N都需要完全重启才能生效",
"f1": "你可以查看:",
"g1": "你可以上传到:",
"cc1": "开关等",
"h1": "关闭 k304",
"i1": "开启 k304",
"j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。",
"k1": "重置设置",
"l1": "登录:",
"m1": "欢迎回来,",
"n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌",
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
"p1": "403: 访问被拒绝 &nbsp;~┻━┻",
"q1": '尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
"r1": "回家",
".s1": "映射",
"t1": "操作",
"u2": "自上次服务器写入的时间$N( 上传 / 重命名 / ... )$N$N17d = 17 天$N1h23 = 1 小时 23 分钟$N4m56 = 4 分钟 56 秒",
"v1": "连接",
"v2": "将此服务器用作本地硬盘$N$N警告这将显示你的密码",
"w1": "切换到 https",
"x1": "更改密码",
"y1": "你的分享",
"z1": "解锁区域",
"ta1": "请先输入新密码",
"ta2": "重复以确认新密码:",
"ta3": "发现拼写错误;请重试",
"aa1": "正在接收的文件:", //m
} }
}; };
var LANGS = ["eng", "nor"];
if (window.langmod) if (window.langmod)
langmod(); langmod();
var d = Ls[sread("cpp_lang", LANGS) || lang] || Ls.eng || Ls.nor; var d = Ls[sread("cpp_lang", Object.keys(Ls)) || lang] ||
Ls.eng || Ls.nor || Ls.chi;
for (var k in (d || {})) { for (var k in (d || {})) {
var f = k.slice(-1), var f = k.slice(-1),
@@ -74,3 +120,42 @@ if (o && /[0-9]+$/.exec(o.innerHTML))
o.innerHTML = shumantime(o.innerHTML); o.innerHTML = shumantime(o.innerHTML);
ebi('uhash').value = '' + location.hash; ebi('uhash').value = '' + location.hash;
(function() {
if (!ebi('x'))
return;
var pwi = ebi('lp');
function redo(msg) {
modal.alert(msg, function() {
pwi.value = '';
pwi.focus();
});
}
function mok(v) {
if (v !== pwi.value)
return redo(d.ta3);
pwi.setAttribute('name', 'pw');
ebi('la').value = 'chpw';
ebi('lf').submit();
}
function stars() {
var m = ebi('modali');
function enstars(n) {
setTimeout(function() { m.value = ''; }, n);
}
m.setAttribute('type', 'password');
enstars(17);
enstars(32);
enstars(69);
}
ebi('x').onclick = function (e) {
ev(e);
if (!pwi.value)
return redo(d.ta1);
modal.prompt(d.ta2, "y", mok, null, stars);
};
})();

View File

@@ -385,6 +385,7 @@ html.y textarea:focus {
} }
.mdo pre, .mdo pre,
.mdo code, .mdo code,
.mdo code[class*="language-"],
.mdo tt { .mdo tt {
font-family: 'scp', monospace, monospace; font-family: 'scp', monospace, monospace;
font-family: var(--font-mono), 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace;

View File

@@ -152,12 +152,13 @@ function U2pvis(act, btns, uc, st) {
r.mod0 = null; r.mod0 = null;
var markup = { var markup = {
'404': '<span class="err">404</span>', '404': '<span class="err">' + L.utl_404 + '</span>',
'ERROR': '<span class="err">ERROR</span>', 'ERROR': '<span class="err">' + L.utl_err + '</span>',
'OS-error': '<span class="err">OS-error</span>', 'OS-error': '<span class="err">' + L.utl_oserr + '</span>',
'found': '<span class="inf">found</span>', 'found': '<span class="inf">' + L.utl_found + '</span>',
'YOLO': '<span class="inf">YOLO</span>', 'defer': '<span class="inf">' + L.utl_defer + '</span>',
'done': '<span class="ok">done</span>', 'YOLO': '<span class="inf">' + L.utl_yolo + '</span>',
'done': '<span class="ok">' + L.utl_done + '</span>',
}; };
r.addfile = function (entry, sz, draw) { r.addfile = function (entry, sz, draw) {
@@ -445,9 +446,7 @@ function U2pvis(act, btns, uc, st) {
return; return;
r.npotato = 0; r.npotato = 0;
var html = [ var html = [L.u_pott.format(r.ctr.ok, r.ctr.ng, r.ctr.bz, r.ctr.q)];
"<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)];
while (r.head < r.tab.length && has(["ok", "ng"], r.tab[r.head].in)) while (r.head < r.tab.length && has(["ok", "ng"], r.tab[r.head].in))
r.head++; r.head++;
@@ -602,7 +601,7 @@ function U2pvis(act, btns, uc, st) {
if (nf < 9000) if (nf < 9000)
return go(); 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) { catch (ex) {
document.body.ondragenter = document.body.ondragleave = document.body.ondragover = null; 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) if (btn)
return; return;
@@ -1104,7 +1103,7 @@ function up2k_init(subtle) {
} }
if (!good_files.length && bad_files.length) 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); return read_dirs(null, [], [], good_files, nil_files, bad_files);
} }
@@ -1122,7 +1121,7 @@ function up2k_init(subtle) {
if (err) if (err)
return modal.alert('sorry, ' + 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)) if ((dz == 'up_dz' && uc.fsearch) || (dz == 'srch_dz' && !uc.fsearch))
tgl_fsearch(); tgl_fsearch();
@@ -1210,7 +1209,7 @@ function up2k_init(subtle) {
match = false; match = false;
if (match) { 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++) for (var a = 0; a < Math.min(20, missing.length); a++)
msg.push('<li>' + esc(missing[a]) + '</li>'); msg.push('<li>' + esc(missing[a]) + '</li>');
@@ -1281,7 +1280,7 @@ function up2k_init(subtle) {
} }
function gotallfiles(good_files, nil_files, bad_files) { function gotallfiles(good_files, nil_files, bad_files) {
if (toast.txt == 'Scanning files...') if (toast.txt == L.u_scan)
toast.hide(); toast.hide();
if (uc.fsearch && !uc.turbo) if (uc.fsearch && !uc.turbo)
@@ -1437,7 +1436,7 @@ function up2k_init(subtle) {
if (!actx || actx.state != 'suspended' || toast.visible) if (!actx || actx.state != 'suspended' || toast.visible)
return; 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); }, 500);
} }
@@ -1479,7 +1478,7 @@ function up2k_init(subtle) {
ev(e); ev(e);
var txt = linklist(); var txt = linklist();
cliptxt(txt + '\n', function () { 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; 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; var nprev = -1;
for (var a = 0; a < st.todo.upload.length; a++) { for (var a = 0; a < st.todo.upload.length; a++) {
var nf = st.todo.upload[a].nfile; var nf = st.todo.upload[a].nfile;
@@ -2255,6 +2246,9 @@ function up2k_init(subtle) {
if (keepalive) if (keepalive)
console.log("sending keepalive handshake", t.name, t); 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(); var xhr = new XMLHttpRequest();
xhr.onerror = xhr.ontimeout = function () { xhr.onerror = xhr.ontimeout = function () {
if (t.t_busied != me) // t.done ok if (t.t_busied != me) // t.done ok
@@ -2281,7 +2275,8 @@ function up2k_init(subtle) {
apop(st.busy.handshake, t); apop(st.busy.handshake, t);
st.todo.handshake.unshift(t); st.todo.handshake.unshift(t);
t.cooldown = Date.now() + 5000 + Math.floor(Math.random() * 3000); 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(); t.t_handshake = Date.now();
@@ -2460,6 +2455,7 @@ function up2k_init(subtle) {
pvis.seth(t.n, 2, L.u_ehstmp, t); pvis.seth(t.n, 2, L.u_ehstmp, t);
var err = "", var err = "",
cls = "ERROR",
rsp = unpre(xhr.responseText), rsp = unpre(xhr.responseText),
ofs = rsp.lastIndexOf('\nURL: '); ofs = rsp.lastIndexOf('\nURL: ');
@@ -2489,6 +2485,8 @@ function up2k_init(subtle) {
if (!t.rechecks && (err_pend || err_srcb)) { if (!t.rechecks && (err_pend || err_srcb)) {
t.rechecks = 0; t.rechecks = 0;
t.want_recheck = true; t.want_recheck = true;
err = L.u_dupdefer;
cls = 'defer';
} }
} }
if (rsp.indexOf('server HDD is full') + 1) if (rsp.indexOf('server HDD is full') + 1)
@@ -2498,7 +2496,7 @@ function up2k_init(subtle) {
if (!t.t_uploading) if (!t.t_uploading)
st.bytes.finished += t.size; st.bytes.finished += t.size;
pvis.seth(t.n, 1, "ERROR"); pvis.seth(t.n, 1, cls);
pvis.seth(t.n, 2, err); pvis.seth(t.n, 2, err);
pvis.move(t.n, 'ng'); pvis.move(t.n, 'ng');
@@ -2530,7 +2528,8 @@ function up2k_init(subtle) {
xhr.open('POST', t.purl, true); xhr.open('POST', t.purl, true);
xhr.responseType = 'text'; 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)); xhr.send(JSON.stringify(req));
} }
@@ -2625,7 +2624,7 @@ function up2k_init(subtle) {
t.nojoin = t.nojoin || t.postlist.length; t.nojoin = t.nojoin || t.postlist.length;
console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t); console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t);
if (!toast.visible && st.todo.upload.length < 4) if (!toast.visible && st.todo.upload.length < 4)
toast.msg(10, L.u_cbusy); toast.inf(10, L.u_cbusy);
} }
else { else {
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)", "warn", t); xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)", "warn", t);

View File

@@ -473,6 +473,24 @@ function crc32(str) {
} }
function randstr(len) {
var ret = '';
try {
var ar = new Uint32Array(Math.floor((len + 3) / 4));
crypto.getRandomValues(ar);
for (var a = 0; a < ar.length; a++)
ret += ('000' + ar[a].toString(36)).slice(-4);
return ret.slice(0, len);
}
catch (ex) {
console.log('using unsafe randstr because ' + ex);
while (ret.length < len)
ret += ('000' + Math.floor(Math.random() * 1679616).toString(36)).slice(-4);
return ret.slice(0, len);
}
}
function clmod(el, cls, add) { function clmod(el, cls, add) {
if (!el) if (!el)
return false; return false;

View File

@@ -1,3 +1,251 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 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
## new features
* #94 @ultwcz translated the UI to Chinese (thx!) 92edea1d
* #84 improvements to [shares](https://github.com/9001/copyparty#shares): 8122dded
* if one or more files are selected for sharing, they are placed into a virtual folder
* more appropriate password UI for accessing protected shares
* human-readable timestamps in shares listing
* u2c (commandline uploader): support multiple exclusion patterns f356faa2
## bugfixes
* remove confusing logmessage when downloading a zerobyte file 9f034d9c
* shares: 7ff46966
* fix crash if the root volume is unmapped
* log-spam on config reload
* password coalescing
* add chrome support
## other changes
* #93 add html IDs to the tabstrip 461f3158
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0819-0014 `v1.14.1` one step forward
[if i turn back now, then this will always follow... one step forward, forward](https://youtu.be/xe3Wkzc0O3k?t=27)
* 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)
## new features
* #92 users can change their own passwords 83fb569d 00da7440
* this feature is default-disabled; see [readme](https://github.com/9001/copyparty#user-changeable-passwords)
* #84 share files/folders by creating a temporary url 7c2beba5
* inspired by other file servers; click the share-button to create a link like `example.com/share/enkz8g374o8g`
* primary usecase is to sneak past authentication services (see issue description)
* the create-share UI has options to accept uploads into the share, and/or set expiration time
* this feature is default-disabled; see [readme](https://github.com/9001/copyparty#shares)
## bugfixes
* #93 fixes for vproxy / location-based / not-vhost-based reverse-proxying 0b46b1a6
* using `--rp-loc` to reverse-proxy from a subfolder made some UI stuff break
* listening on unix-sockets: 687df2fa
* fix `x-forwarded-for` support, and avoid a possible container-specific collision
* new syntax which allows setting unix-permissions and unix-group
* `-i unix:770:www:/tmp/party.sock` (see `--help-bind` for more examples)
* using relocation hooks (introduced in previous ver) could cause dedup issues c8f4aeae b0af4b37
* custom fonts using `@import` css statements 5a62cb48
* invert volume scrollwheel 7d8d9438
## other changes
* changed the button colors in theme 2 (pm-monokai) from red to yellow 5153db6b
* the red buttons look better, but are too confusing because usually red means off
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0813-0008 `v1.13.8` hook into place
## new features
* #86 intentional side-effects from hooks 6c94a63f
* use hooks (plugins) to conditionally move uploads into another folder depending on filename, extension, uploader ip/name, file contents, ...
* hooks can create additional files and tell copyparty to index them immediately, or delete an existing file based on some condition
* only one example so far though, [reloc-by-ext](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#before-upload) which was a feature-request to dodge [sharex#3992](https://github.com/ShareX/ShareX/issues/3992)
* listen on unix-sockets ee9aad82
* `-i unix:/tmp/party.sock` stops listening on TCP ports entirely, and only listens on that unix-socket
* can be combined with regular sockets, `-i 127.0.0.1,unix:/tmp/a.sock`
* kinda buggy for now (need to `--xff-src=any` and doesn't let you set socket-perms yet), will be fixed in next ver
* makes it 10% faster, but more importantly offers tighter access control behind reverse-proxies
* inspired by https://www.oligo.security/blog/0-0-0-0-day-exploiting-localhost-apis-from-the-browser
* up2k stitching:
* more optimal stitch sizes for max throughput across connections c862ec1b
* improve fat32 compatibility 373194c3
* new option `--js-other` to load custom javascript dbd42bc6
* `--js-browser` affects the filebrowser page, `--js-other` does all the others
* endless possibilities, such as [adding a login-banner](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/banner.js) which [looks like this](https://github.com/user-attachments/assets/8ae8e087-b209-449c-b08d-74e040f0284b)
* list detected optional dependencies on startup 3db117d8
* hopefully reduces the guesswork / jank factor by a tiny bit
## bugfixes
* up2k stitching:
* put the request headers on a diet so they fit through more reverse-proxies 0da719f4
* fix deadlock on s390x (IBM mainframes) 250c8c56
## other changes
* add flags to disengage [features](https://github.com/9001/copyparty/tree/hovudstraum#feature-chickenbits) and [dependencies](https://github.com/9001/copyparty/tree/hovudstraum#dependency-chickenbits) in case they cause trouble 72361c99
* optimizations
* 6% faster on average d5c9c8eb
* docker: reduce ram usage 98ffaadf
* python2: reduce ram usage ebb19818
* docker: add [portainer howto](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/portainer.md) e136231c
* update deps ca001c85
* pyftpdlib 1.5.10
* copyparty.exe: python 3.12.5
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-0729-2028 `v1.13.6` not that big # 2024-0729-2028 `v1.13.6` not that big
@@ -747,7 +995,7 @@ dnf install https://ocv.me/copyparty/fedora/39/python3-copyparty.fc39.noarch.rpm
## other changes ## other changes
* improved [systemd example](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd) with hardening and a better example config * 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 * 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 * 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
@@ -1016,12 +1264,12 @@ okay, i swear this is the last version for weeks! probably
* [r0c is much better](https://github.com/9001/r0c) than this joke * [r0c is much better](https://github.com/9001/r0c) than this joke
## bugfixes ## 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, * 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-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 * `--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) * 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)
* 3b8f66c0d5c27a68841814ec06f1758f146a5ff5 javascript could crash while uploading from a very unreliable internet connection * 3b8f66c0 javascript could crash while uploading from a very unreliable internet connection
## other changes ## other changes
* copyparty.exe: updated pillow to 10.0.1 which fixes the webp cve * copyparty.exe: updated pillow to 10.0.1 which fixes the webp cve
@@ -1067,7 +1315,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` * default compression levels are gz:3, bz2:2, xz:1; override with `?tar=gz:9`
# bugfixes # 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 * 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 * when linking [a search query](https://a.ocv.me/pub/#q=tags%20like%20soundsho*) it loads the results faster
@@ -1082,12 +1330,12 @@ hello! it's been a while, an entire day even...
## new features ## new features
* iPhones and iPads are now able to... * 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) * 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 * by translating [smart-quotes](https://stackoverflow.com/questions/48678359/ios-11-safari-html-disable-smart-punctuation) into regular `'` and `"` characters
* python 3.12 support * 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 * 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 * print more context in server logs if someone hits an unexpected permission-denied
@@ -1308,8 +1556,8 @@ Thanks for flying copyparty! And especially if you decide to continue doing so :
```bash ```bash
(gzip -dc access.log.*.gz; cat access.log) | sed -r 's/" [0-9]+ .*//' | grep -E 'cpr/.*%2[^0]' | grep -vF data:image/svg (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) * 77f1e514 fixes an extremely unlikely race-condition (see the commit for details)
* 8f59afb1593a75b8ce8c91ceee304097a07aea6e fixes another race-condition which is a bit worse: * 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 * 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
---- ----
@@ -1483,7 +1731,7 @@ don't get excited! nothing new and revolutionary, but `xvol` and `xdev` changed
# 2023-0426-2300 `v1.6.15` unexpected boost # 2023-0426-2300 `v1.6.15` unexpected boost
## new features ## 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 * 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 * 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` * **webdav:** enabled by default (because rclone will want this), can be disabled with arg `--dav-rt` or volflag `davrt`
@@ -1687,7 +1935,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 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 * 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) * *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
---- ----
@@ -1894,7 +2142,7 @@ but nothing is affected (that i know of):
* tar/zip-download of hidden folders * tar/zip-download of hidden folders
* unpost filtering was buggy for non-ascii characters * unpost filtering was buggy for non-ascii characters
* moving a deduplicated file on a volume where deduplication was since disabled * 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 * add custom text selection colors because chrome is currently broken on fedora
* blockdevs (`/dev/nvme0n1`) couldn't be downloaded as files * blockdevs (`/dev/nvme0n1`) couldn't be downloaded as files
* misc fixes for location-based reverse-proxying * misc fixes for location-based reverse-proxying
@@ -1923,7 +2171,7 @@ hello from warsaw airport (goodbye japan ;_;)
* browser ui didn't allow specifying number of threads for file search * browser ui didn't allow specifying number of threads for file search
* dont panic if a digit key is pressed while viewing an image * 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 * 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 * they fixed it in linux 6.1 so these workarounds will be gone too
@@ -2889,7 +3137,7 @@ fixed another dumdum, sorry for the spam
* the ftp server is not compatible with python 3.12 (releasing october 2023) * 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) * 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
@@ -3670,7 +3918,7 @@ we did it reddit 👉😎👉
* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18) * 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) * 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 ## new features
* provide password using basic-authentication * provide password using basic-authentication
@@ -5140,7 +5388,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 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` * this bug was introduced 19 days ago, in `v0.10.17`
* the requirement to be affected is write-only folders mounted within readable folders * 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` * also fixes minor bugs introduced in `v0.11.1`
* this version is the same as `v0.11.5` on pypi * this version is the same as `v0.11.5` on pypi
@@ -5324,8 +5572,8 @@ in other news, minor ui tweaks:
* a few lightmode adjustments * a few lightmode adjustments
* less cpu usage? should be * less cpu usage? should be
`copyparty-sfx.py` (latest) made from c5db7c1a0c8f6ab23138ad7ea7642a6260e7da9b (v0.10.15-15) fixes `-j` (multiprocessing/high-performance) `copyparty-sfx.py` (latest) made from c5db7c1a (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-5a579db.py` (old) made from 5a579dba (v0.10.15-5) reduced the size
@@ -5638,7 +5886,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) 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 other changes include
* support chunked PUT requests from curl * support chunked PUT requests from curl
@@ -5858,7 +6106,7 @@ valvrave-stop.jpg
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2020-0818-1822 `v0.5.2` da setter vi punktum # 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

@@ -1,7 +1,7 @@
## devnotes toc ## devnotes toc
* top * top
* [future plans](#future-plans) - some improvement ideas * [future ideas](#future-ideas) - list of dreams which will probably never happen
* [design](#design) * [design](#design)
* [up2k](#up2k) - quick outline of the up2k protocol * [up2k](#up2k) - quick outline of the up2k protocol
* [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/) * [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/)
@@ -27,9 +27,9 @@
* [discarded ideas](#discarded-ideas) * [discarded ideas](#discarded-ideas)
# future plans # future ideas
some improvement ideas list of dreams which will probably never happen
* the JS is a mess -- a ~~preact~~ rewrite would be nice * the JS is a mess -- a ~~preact~~ rewrite would be nice
* preferably without build dependencies like webpack/babel/node.js, maybe a python thing to assemble js files into main.js * preferably without build dependencies like webpack/babel/node.js, maybe a python thing to assemble js files into main.js
@@ -139,6 +139,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| GET | `?tar&w` | pregenerate webp thumbnails | | GET | `?tar&w` | pregenerate webp thumbnails |
| GET | `?tar&j` | pregenerate jpg thumbnails | | GET | `?tar&j` | pregenerate jpg thumbnails |
| GET | `?tar&p` | pregenerate audio waveforms | | GET | `?tar&p` | pregenerate audio waveforms |
| GET | `?shares` | list your shared files/folders |
| GET | `?ups` | show recent uploads from your IP | | GET | `?ups` | show recent uploads from your IP |
| GET | `?ups&filter=f` | ...where URL contains `f` | | GET | `?ups&filter=f` | ...where URL contains `f` |
| GET | `?mime=foo` | specify return mimetype `foo` | | GET | `?mime=foo` | specify return mimetype `foo` |
@@ -175,6 +176,9 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) | | mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) |
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL | | mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
| POST | `?delete` | | delete URL recursively | | POST | `?delete` | | delete URL recursively |
| POST | `?eshare=rm` | | stop sharing a file/folder |
| POST | `?eshare=3` | | set expiration to 3 minutes |
| jPOST | `?share` | (complicated) | create temp URL for file/folder |
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively | | jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |
| uPOST | | `msg=foo` | send message `foo` into server log | | uPOST | | `msg=foo` | send message `foo` into server log |
| mPOST | | `act=tput`, `body=TEXT` | overwrite markdown document at URL | | mPOST | | `act=tput`, `body=TEXT` | overwrite markdown document at URL |

View File

@@ -16,7 +16,7 @@ open up notepad and save the following as `c:\users\you\documents\party.conf` (f
```yaml ```yaml
[global] [global]
lo: ~/logs/cpp-%Y-%m%d.xz # log to c:\users\you\logs\ 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 p: 80, 443 # listen on ports 80 and 443, not 3923
theme: 2 # default theme: protonmail-monokai theme: 2 # default theme: protonmail-monokai
lang: nor # default language: viking 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] ### 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) * `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 * `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 * 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 * `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` * `p: 80,443` listens on the ports `80` and `443` instead of the default `3923`

210
docs/logo.svg Normal file
View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="300mm"
height="207mm"
viewBox="0 0 300 207"
version="1.1"
id="svg1"
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"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title1">copyparty_logo</title>
<defs
id="defs1">
<linearGradient
inkscape:collect="always"
id="linearGradient1">
<stop
style="stop-color:#ffcc55;stop-opacity:1"
offset="0"
id="stop1" />
<stop
style="stop-color:#ffcc00;stop-opacity:1"
offset="0.2"
id="stop2" />
<stop
style="stop-color:#ff8800;stop-opacity:1"
offset="1"
id="stop3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="15"
y1="15"
x2="15"
y2="143"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
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>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="kassett">
<rect
style="fill:#333333"
id="rect1"
width="300"
height="205"
x="0"
y="0"
rx="12"
ry="12" />
<rect
style="fill:url(#linearGradient2)"
id="rect2"
width="270"
height="128"
x="15"
y="15"
rx="8"
ry="8" />
<rect
style="fill:#333333"
id="rect3"
width="172"
height="52"
x="64"
y="72"
rx="26"
ry="26" />
<circle
style="fill:#cccccc"
id="circle1"
cx="91"
cy="98"
r="18" />
<circle
style="fill:#cccccc"
id="circle2"
cx="209"
cy="98"
r="18" />
<path
style="fill:#737373;stroke-width:1px"
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="layer3"
inkscape:label="tekst"
style="display:none">
<text
xml:space="preserve"
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
x="47.153069"
y="55.548954"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
x="47.153069"
y="55.548954"
style="-inkscape-font-specification:Akbar"
rotate="0 0">copyparty</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="stensatt">
<path
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 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 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,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 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,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 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 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 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>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagger">
<g
id="g1">
<path
id="path4"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path5"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path6"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
<g
id="g2"
transform="rotate(30,150,318.19)">
<path
id="path7"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path8"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path9"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -141,6 +141,9 @@ find -maxdepth 1 -printf '%s %p\n' | sort -n | awk '!/-([0-9a-zA-Z_-]{11})\.(mkv
# unique stacks in a stackdump # unique stacks in a stackdump
f=a; rm -rf stacks; mkdir stacks; grep -E '^#' $f | while IFS= read -r n; do awk -v n="$n" '!$0{o=0} o; $0==n{o=1}' <$f >stacks/f; h=$(sha1sum <stacks/f | cut -c-16); mv stacks/f stacks/$h-"$n"; done ; find stacks/ | sort | uniq -cw24 f=a; rm -rf stacks; mkdir stacks; grep -E '^#' $f | while IFS= read -r n; do awk -v n="$n" '!$0{o=0} o; $0==n{o=1}' <$f >stacks/f; h=$(sha1sum <stacks/f | cut -c-16); mv stacks/f stacks/$h-"$n"; done ; find stacks/ | sort | uniq -cw24
# find unused css variables
cat browser.css | sed -r 's/(var\()/\n\1/g' | awk '{sub(/:/," ")} $1~/^--/{d[$1]=1} /var\(/{sub(/.*var\(/,"");sub(/\).*/,"");u[$1]=1} END{for (x in u) delete d[x]; for (x in d) print x}' | tr '\n' '|'
## ##
## sqlite3 stuff ## sqlite3 stuff
@@ -252,6 +255,9 @@ 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 # 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 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
# py2 on osx # py2 on osx
brew install python@2 brew install python@2
pip install virtualenv pip install virtualenv

View File

@@ -63,6 +63,8 @@ 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 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) 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 * make sure to keep backups of your work religiously! since that location is volatile af

View File

@@ -8,7 +8,7 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
ENV XDG_CONFIG_HOME=/cfg ENV XDG_CONFIG_HOME=/cfg
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc \
wget \ tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow \ py3-jinja2 py3-argon2-cffi py3-pillow \
ffmpeg 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-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/ COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U !pyc \ RUN apk add -U !pyc \
wget \ tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \ py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \
ffmpeg \ ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \ 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 ENV XDG_CONFIG_HOME=/cfg
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc \
wget \ tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow py3-mutagen py3-jinja2 py3-argon2-cffi py3-pillow py3-mutagen
COPY i/dist/copyparty-sfx.py innvikler.sh ./ 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 ENV XDG_CONFIG_HOME=/cfg
RUN apk add -U !pyc \ RUN apk add -U !pyc \
wget \ tzdata wget \
py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \ py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \
ffmpeg \ ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \ vips-jxl vips-heif vips-poppler vips-magick \

View File

@@ -100,11 +100,11 @@ filt=
aa="$(printf '%11s' $a-$i)" aa="$(printf '%11s' $a-$i)"
# arm takes forever so make it top priority # 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 # --pull=never does nothing at all btw
(set -x (set -x
$nice podman build \ nice $nice podman build \
--squash \ --squash \
--pull=never \ --pull=never \
--from localhost/alpine-$a \ --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: # to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
_ = r"""" _ = 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 ./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
""" """
# click [edit] => [select all] # click [edit] => [select all]

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: # 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 ./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 # then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c

View File

@@ -3,6 +3,7 @@ set -e
echo echo
berr() { p=$(head -c 72 </dev/zero | tr '\0' =); printf '\n%s\n\n' $p; cat; printf '\n%s\n\n' $p; } berr() { p=$(head -c 72 </dev/zero | tr '\0' =); printf '\n%s\n\n' $p; cat; printf '\n%s\n\n' $p; }
aerr() { printf '%s\n' "$*" | berr; }
help() { exec cat <<'EOF' help() { exec cat <<'EOF'
@@ -28,9 +29,11 @@ help() { exec cat <<'EOF'
# #
# `no-tfp` saves ~10k by removing the tftp server, disabling --tftp # `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-smb` saves ~3.5k by removing the smb / cifs server
# #
# `no-zm` saves ~k by removing the zeroconf mDNS server # `no-pf` saves ~2.8k by removing the option to download partyfuse
# #
# _____________________________________________________________________ # _____________________________________________________________________
# web features: # web features:
@@ -52,10 +55,15 @@ help() { exec cat <<'EOF'
# #
# `ign-wd` allows building an sfx without webdeps # `ign-wd` allows building an sfx without webdeps
# #
# --------------------------------------------------------------------- # _____________________________________________________________________
#
# if you are on windows, you can use msys2: # if you are on windows, you can use msys2:
# PATH=/c/Users/$USER/AppData/Local/Programs/Python/Python310:"$PATH" ./make-sfx.sh fast # PATH=/c/Users/$USER/AppData/Local/Programs/Python/Python310:"$PATH" ./make-sfx.sh fast
#
# _____________________________________________________________________
# some usage examples:
# ./scripts/make-sfx.sh lang eng no-cm no-hl no-dd no-fnt no-smb no-pf
# ./scripts/rls.sh sfx lang eng no-cm no-hl no-dd no-fnt no-smb no-pf
# (reduces v1.14.2 from 700k to 495k)
EOF EOF
} }
@@ -112,6 +120,7 @@ while [ ! -z "$1" ]; do
no-tfp) no_tfp=1 ; ;; no-tfp) no_tfp=1 ; ;;
no-smb) no_smb=1 ; ;; no-smb) no_smb=1 ; ;;
no-zm) no_zm=1 ; ;; no-zm) no_zm=1 ; ;;
no-pf) no_pf=1 ; ;;
no-fnt) no_fnt=1 ; ;; no-fnt) no_fnt=1 ; ;;
no-hl) no_hl=1 ; ;; no-hl) no_hl=1 ; ;;
no-dd) no_dd=1 ; ;; no-dd) no_dd=1 ; ;;
@@ -119,7 +128,6 @@ while [ ! -z "$1" ]; do
dl-wd) dl_wd=1 ; ;; dl-wd) dl_wd=1 ; ;;
ign-wd) ign_wd=1 ; ;; ign-wd) ign_wd=1 ; ;;
fast) zopf= ; ;; fast) zopf= ; ;;
ultra) ultra=1 ; ;;
lang) shift;langs="$1"; ;; lang) shift;langs="$1"; ;;
*) help ; ;; *) help ; ;;
esac esac
@@ -428,6 +436,9 @@ rm -f ftp/pyftpdlib/{__main__,prefork}.py
[ $no_zm ] && [ $no_zm ] &&
rm -rf copyparty/mdns.py copyparty/stolen/dnslib rm -rf copyparty/mdns.py copyparty/stolen/dnslib
[ $no_pf ] &&
rm -rf copyparty/web/a/partyfuse.py
[ $no_cm ] && { [ $no_cm ] && {
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde* rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
echo h > copyparty/web/mde.html echo h > copyparty/web/mde.html
@@ -451,11 +462,16 @@ rm -f ftp/pyftpdlib/{__main__,prefork}.py
ised 's/(cursor: ?)url\([^)]+\), ?(pointer)/\1\2/; s/[0-9]+% \{cursor:[^}]+\}//; s/animation: ?cursor[^};]+//' $f ised 's/(cursor: ?)url\([^)]+\), ?(pointer)/\1\2/; s/[0-9]+% \{cursor:[^}]+\}//; s/animation: ?cursor[^};]+//' $f
} }
[ $langs ] && [ $langs ] && {
echo $langs | grep -q eng || {
langs="eng|$langs"
aerr "ERROR: removing english is not supported; will do this instead: $langs"
}
for f in copyparty/web/{browser.js,splash.js}; do for f in copyparty/web/{browser.js,splash.js}; do
gzip -d "$f.gz" || true gzip -d "$f.gz" || true
iawk '/^\}/{l=0} !l; /^var Ls =/{l=1;next} o; /^\t["}]/{o=0} /^\t"'"$langs"'"/{o=1;print}' $f iawk '/^\}/{l=0} !l; /^var Ls =/{l=1;next} !l{next} o; /^\t["}]/{o=0} /^\t"'"$langs"'"/{o=1;print}' $f
done done
}
[ ! $repack ] && { [ ! $repack ] && {
# uncomment # uncomment
@@ -593,7 +609,7 @@ pc="bzip2 -"; pe=bz2
[ $use_gzz ] && pc="pigz -11 -I$use_gzz" && pe=gz [ $use_gzz ] && pc="pigz -11 -I$use_gzz" && pe=gz
echo compressing tar 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 minf=$(for f in t.*.$pe; do
s1=$(wc -c <$f) s1=$(wc -c <$f)
s2=$(tr -d '\r\n\0' <$f | wc -c) s2=$(tr -d '\r\n\0' <$f | wc -c)

View File

@@ -34,6 +34,6 @@ d1420c8417fad7888766dd26b9706a87c63e8f33dceeb8e26d0056d5127b0b3ed9272e44b4b76113
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl 8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
0203ec2551c4836696cfab0b2c9fff603352f03fa36e7476e2e1ca7ec57a3a0c24bd791fcd92f342bf817f0887854d9f072e0271c643de4b313d8c9569ba8813 packaging-24.1-py3-none-any.whl 0203ec2551c4836696cfab0b2c9fff603352f03fa36e7476e2e1ca7ec57a3a0c24bd791fcd92f342bf817f0887854d9f072e0271c643de4b313d8c9569ba8813 packaging-24.1-py3-none-any.whl
2be320b4191f208cdd6af183c77ba2cf460ea52164ee45ac3ff17d6dfa57acd9deff016636c2dd42a21f4f6af977d5f72df7dacf599bebcf41757272354d14c1 pillow-10.4.0-cp312-cp312-win_amd64.whl 2be320b4191f208cdd6af183c77ba2cf460ea52164ee45ac3ff17d6dfa57acd9deff016636c2dd42a21f4f6af977d5f72df7dacf599bebcf41757272354d14c1 pillow-10.4.0-cp312-cp312-win_amd64.whl
776378f5414efd26ec8a1cb3228a7b5fdf6afca3fa335a0e9b071266d55d9d9e66ee157c25a468a05bfa70ccd33c48b101998523fc6ff6bcf5e82a1d81ed0af8 pyinstaller-6.9.0-py3-none-win_amd64.whl 896ddddbd4b85e86e0600cb65eb4c07fbc7f3802d47e7f660411e20b5500831469b97ed4770f25820f4e75cbfac40308da624fd86d4f62e578149d5c276a9cde pyinstaller-6.10.0-py3-none-win_amd64.whl
c0af77d2a57cb063ab038dc986ed3582bc5acc8c8bd91d726101935d6388f50854ddbca26bc846ed5d1022cdee4d96242938c66f0ddc4565c36b60d691064db8 pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl 873781decaeef07f6a79b0ed8b9f35f3fa534a1ea0d866991e40278a10818fa5b60c70b0d5828971b045364f1099694cd1e5d5d60d480acb93fcfbfbced4a09e pyinstaller_hooks_contrib-2024.8-py3-none-any.whl
0572c6345f6a4f7f3e5c2ff858e3ca7ca54ae4478f3d59d8e18cb0f596e61dcf12aef579db229e83d63b30f15d6684ee6bb3feaea9413e5e636a503933057678 python-3.12.5-amd64.exe 37fa7250b10b0c03b87d800bf4f920589649309cb4fbd25864475084bb7873d62b809a4fdeabd06c79f03f33614218eb7e01a9bd796de29dd3b141f1906d588c python-3.12.6-amd64.exe

View File

@@ -39,9 +39,9 @@ fns=(
mutagen-1.47.0-py3-none-any.whl mutagen-1.47.0-py3-none-any.whl
packaging-24.1-py3-none-any.whl packaging-24.1-py3-none-any.whl
pillow-10.4.0-cp312-cp312-win_amd64.whl pillow-10.4.0-cp312-cp312-win_amd64.whl
pyinstaller-6.9.0-py3-none-win_amd64.whl pyinstaller-6.10.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl pyinstaller_hooks_contrib-2024.8-py3-none-any.whl
python-3.12.5-amd64.exe python-3.12.6-amd64.exe
) )
[ $w7 ] && fns+=( # u2c stuff [ $w7 ] && fns+=( # u2c stuff
certifi-2024.2.2-py3-none-any.whl certifi-2024.2.2-py3-none-any.whl

View File

@@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash
set -e set -e
# if specified, keep the following sfx flags last: gz gzz fast
parallel=1 parallel=1
[ -e make-sfx.sh ] || cd scripts [ -e make-sfx.sh ] || cd scripts
@@ -35,6 +37,14 @@ f=../dist/copyparty-sfx
$f$s.py --version >/dev/null $f$s.py --version >/dev/null
while [ "$1" ]; do
case "$1" in
gz*) break;;
fast) break;;
esac
shift
done
[ $parallel -gt 1 ] && { [ $parallel -gt 1 ] && {
printf '\033[%s' s 2r H "0;1;37;44mbruteforcing sfx size -- press enter to terminate" K u "7m $* " K $'27m\n' printf '\033[%s' s 2r H "0;1;37;44mbruteforcing sfx size -- press enter to terminate" K u "7m $* " K $'27m\n'
trap "rm -f .sfx-run; printf '\033[%s' s r u" INT TERM EXIT trap "rm -f .sfx-run; printf '\033[%s' s r u" INT TERM EXIT

View File

@@ -103,6 +103,9 @@ copyparty/web/mde.html,
copyparty/web/mde.js, copyparty/web/mde.js,
copyparty/web/msg.css, copyparty/web/msg.css,
copyparty/web/msg.html, copyparty/web/msg.html,
copyparty/web/shares.css,
copyparty/web/shares.html,
copyparty/web/shares.js,
copyparty/web/splash.css, copyparty/web/splash.css,
copyparty/web/splash.html, copyparty/web/splash.html,
copyparty/web/splash.js, copyparty/web/splash.js,

View File

@@ -70,6 +70,10 @@ def uh2(fp):
continue continue
on = True on = True
if " # !rm" in ln:
continue
lns.append(ln) lns.append(ln)
cs = "\n".join(lns) cs = "\n".join(lns)

50
scripts/tlcheck.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
set -e
# usage: ./scripts/tlcheck.sh eng chi copyparty/web/browser.js
awk <"$3" -v lang1=\"$1\": -v lang2=\"$2\": '
/^\t\}/{fa=0;fb=0}
!/":/{next}
$0~lang1{fa=1}
$0~lang2{fb=1}
fa{a[ia++]=$0}
fb{b[ib++]=$0}
END{for (i=0;i<ia;i++) printf "%s\n%s\n\n",a[i],b[i]}
' |
awk -v apos=\' -v quot=\" '
# count special chars and prefix to line
function c(ch) {
m=$0;
gsub(ch,"",m);
t=t sprintf("%s%d ", ch, length($0)-length(m))
}
!$0 && t!=tp {
print "\n\033[1;37;41m====DIFF===="
}
!$0 { print; next; }
{
tp=t; t="";
c(quot);
c(apos);
c("<");
c(">");
c("{");
c("}");
c("&");
c("\\\$");
c("\\\\");
print t $0;
}
' |
sed -r $'
s/\\\\/\033[1;37;41m\\\\\033[0m/g;
s/\$N/\033[1;37;45m$N\033[0m/g;
s/([{}])/\033[34m\\1\033[0m/g;
s/"/\033[44m"\033[0m/g;
s/\'/\033[45m\'\033[0m/g;
s/&/\033[1;43;30m&\033[0m/g;
s/([<>])/\033[30;47m\\1\033[0m/g
' |
sed -r 's/\t+//' |
less -R

View File

@@ -20,7 +20,7 @@ cat $f | awk '
o{next} o{next}
/^#/{s=1;rs=0;pr()} /^#/{s=1;rs=0;pr()}
/^#* *(nix package)/{rs=1} /^#* *(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); lv=length($1);
sub(/[^ ]+ /,""); sub(/[^ ]+ /,"");

245
tests/test_dedup.py Normal file
View File

@@ -0,0 +1,245 @@
#!/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.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from tests import util as tu
from tests.util import Cfg
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)
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_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 = self.files
self.ctr = 336 if quick else 2016 # estimated total num uploads
self.conn = None
self.fstab = None
for e2d in [True, 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)
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:
print("using original name " + fn)
else:
print(fn + " got renamed to " + sfn)
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)
elif first:
raise Exception("found first; %r, %r" % ((dn, fn, fi), hs))
h, b = self.curl("%s/%s" % (dn, sfn))
self.assertEqual(b, data)
return sfn
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)
HttpCli(self.conn.setbuf(buf)).run()
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
# print("HS <--", ret)
return ret
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, 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]
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def log(self, src, msg, c=0):
print(msg)

View File

@@ -24,6 +24,10 @@ def hdr(query, uname):
class TestDots(unittest.TestCase): class TestDots(unittest.TestCase):
def __init__(self, *a, **ka):
super(TestDots, self).__init__(*a, **ka)
self.is_dut = True
def setUp(self): def setUp(self):
self.td = tu.get_ramdisk() self.td = tu.get_ramdisk()

View File

@@ -86,6 +86,7 @@ class TestHttpCli(unittest.TestCase):
self.args = Cfg(v=vcfg, a=["o:o", "x:x"]) self.args = Cfg(v=vcfg, a=["o:o", "x:x"])
self.asrv = AuthSrv(self.args, self.log) 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)] vfiles = [x for x in allfiles if x.startswith(top)]
for fp in vfiles: for fp in vfiles:
tctr += 1 tctr += 1
@@ -204,14 +205,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 = "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") buf = buf.format(url, len(url) + 4).encode("utf-8")
print("PUT -->", buf) print("PUT -->", buf)
conn = tu.VHttpConn(self.args, self.asrv, self.log, buf) conn = self.conn.setbuf(buf)
HttpCli(conn).run() HttpCli(conn).run()
ret = conn.s._reply.decode("utf-8").split("\r\n\r\n", 1) ret = conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("PUT <--", ret) print("PUT <--", ret)
return ret return ret
def curl(self, url, binary=False): 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() HttpCli(conn).run()
if binary: if binary:
h, b = conn.s._reply.split(b"\r\n\r\n", 1) h, b = conn.s._reply.split(b"\r\n\r\n", 1)

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,7 +3,7 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os import os
import platform import random
import re import re
import shutil import shutil
import socket import socket
@@ -16,9 +16,7 @@ from argparse import Namespace
import jinja2 import jinja2
WINDOWS = platform.system() == "Windows" from copyparty.__init__ import MACOS, WINDOWS, E
ANYWIN = WINDOWS or sys.platform in ["msys"]
MACOS = platform.system() == "Darwin"
J2_ENV = jinja2.Environment(loader=jinja2.BaseLoader) # type: ignore J2_ENV = jinja2.Environment(loader=jinja2.BaseLoader) # type: ignore
J2_FILES = J2_ENV.from_string("{{ files|join('\n') }}\nJ2EOT") J2_FILES = J2_ENV.from_string("{{ files|join('\n') }}\nJ2EOT")
@@ -42,15 +40,20 @@ if MACOS:
# 25% faster; until any tests do symlink stuff # 25% faster; until any tests do symlink stuff
from copyparty.__init__ import E
from copyparty.__main__ import init_E from copyparty.__main__ import init_E
from copyparty.broker_thr import BrokerThr
from copyparty.ico import Ico from copyparty.ico import Ico
from copyparty.u2idx import U2idx from copyparty.u2idx import U2idx
from copyparty.up2k import Up2k
from copyparty.util import FHC, CachedDict, Garda, Unrecv from copyparty.util import FHC, CachedDict, Garda, Unrecv
init_E(E) init_E(E)
def randbytes(n):
return random.getrandbits(n * 8).to_bytes(n, "little")
def runcmd(argv): def runcmd(argv):
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE) p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
@@ -119,16 +122,16 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0): def __init__(self, a=None, v=None, c=None, **ka0):
ka = {} ka = {}
ex = "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_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" 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_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()}) ka.update(**{k: False for k in ex.split()})
ex = "dotpart dotsrch hook_v no_dhash no_fastboot 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()}) 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 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()}) 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()}) ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol mtab_age reg_cap s_thead s_tbody th_convt" ex = "au_vol mtab_age reg_cap s_thead s_tbody th_convt"
@@ -137,9 +140,12 @@ class Cfg(Namespace):
ex = "db_act k304 loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo" ex = "db_act k304 loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
ka.update(**{k: 0 for k in ex.split()}) ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname doctitle df exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i tcolor textfiles unlist vname R RS SR" ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sbf log_fk md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src R RS SR"
ka.update(**{k: "" for k in ex.split()}) ka.update(**{k: "" for k in ex.split()})
ex = "ban_403 ban_404 ban_422 ban_pw ban_url"
ka.update(**{k: "no" for k in ex.split()})
ex = "grp on403 on404 xad xar xau xban xbd xbr xbu xiu xm" ex = "grp on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()}) ka.update(**{k: [] for k in ex.split()})
@@ -170,6 +176,7 @@ class Cfg(Namespace):
s_wr_sz=256 * 1024, s_wr_sz=256 * 1024,
sort="href", sort="href",
srch_hits=99999, srch_hits=99999,
SRS="/",
th_covers=["folder.png"], th_covers=["folder.png"],
th_coversd=["folder.png"], th_coversd=["folder.png"],
th_covers_set=set(["folder.png"]), th_covers_set=set(["folder.png"]),
@@ -220,11 +227,29 @@ class VSock(object):
pass pass
class VHub(object):
def __init__(self, args, asrv, log):
self.args = args
self.asrv = asrv
self.log = log
self.is_dut = True
self.up2k = Up2k(self)
class VBrokerThr(BrokerThr):
def __init__(self, hub):
self.hub = hub
self.log = hub.log
self.args = hub.args
self.asrv = hub.asrv
class VHttpSrv(object): class VHttpSrv(object):
def __init__(self, args, asrv, log): def __init__(self, args, asrv, log):
self.args = args self.args = args
self.asrv = asrv self.asrv = asrv
self.log = log self.log = log
self.hub = None
self.broker = NullBroker(args, asrv) self.broker = NullBroker(args, asrv)
self.prism = None self.prism = None
@@ -232,7 +257,7 @@ class VHttpSrv(object):
self.nreq = 0 self.nreq = 0
self.nsus = 0 self.nsus = 0
aliases = ["splash", "browser", "browser2", "msg", "md", "mde"] aliases = ["splash", "shares", "browser", "browser2", "msg", "md", "mde"]
self.j2 = {x: J2_FILES for x in aliases} self.j2 = {x: J2_FILES for x in aliases}
self.gpwd = Garda("") self.gpwd = Garda("")
@@ -251,18 +276,25 @@ class VHttpSrv(object):
return self.u2idx return self.u2idx
class VHttpSrvUp2k(VHttpSrv):
def __init__(self, args, asrv, log):
super(VHttpSrvUp2k, self).__init__(args, asrv, log)
self.hub = VHub(args, asrv, log)
self.broker = VBrokerThr(self.hub)
class VHttpConn(object): class VHttpConn(object):
def __init__(self, args, asrv, log, buf): def __init__(self, args, asrv, log, buf, use_up2k=False):
self.t0 = time.time() self.t0 = time.time()
self.s = VSock(buf)
self.sr = Unrecv(self.s, None) # type: ignore
self.aclose = {} self.aclose = {}
self.addr = ("127.0.0.1", "42069") self.addr = ("127.0.0.1", "42069")
self.args = args self.args = args
self.asrv = asrv self.asrv = asrv
self.bans = {} self.bans = {}
self.freshen_pwd = 0.0 self.freshen_pwd = 0.0
self.hsrv = VHttpSrv(args, asrv, log)
Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv
self.hsrv = Ctor(args, asrv, log)
self.ico = Ico(args) self.ico = Ico(args)
self.ipa_nm = None self.ipa_nm = None
self.lf_url = None self.lf_url = None
@@ -278,6 +310,12 @@ class VHttpConn(object):
self.u2fh = FHC() self.u2fh = FHC()
self.get_u2idx = self.hsrv.get_u2idx self.get_u2idx = self.hsrv.get_u2idx
self.setbuf(buf)
def setbuf(self, buf):
self.s = VSock(buf)
self.sr = Unrecv(self.s, None) # type: ignore
return self
if WINDOWS: if WINDOWS: