mirror of
https://github.com/9001/copyparty.git
synced 2025-11-02 04:53:15 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf3941cf7a | ||
|
|
3649e8288a | ||
|
|
9a45e26026 | ||
|
|
e65f127571 | ||
|
|
3bfc699787 | ||
|
|
955318428a | ||
|
|
f6279b356a | ||
|
|
4cc3cdc989 | ||
|
|
f9aa20a3ad | ||
|
|
129d33f1a0 | ||
|
|
1ad7a3f378 | ||
|
|
b533be8818 | ||
|
|
fb729e5166 | ||
|
|
d337ecdb20 | ||
|
|
5f1f0a48b0 | ||
|
|
e0f1cb94a5 | ||
|
|
a362ee2246 | ||
|
|
19f23c686e | ||
|
|
23b20ff4a6 | ||
|
|
72574da834 | ||
|
|
d5a79455d1 | ||
|
|
070d4b9da9 | ||
|
|
0ace22fffe | ||
|
|
9e483d7694 | ||
|
|
26458b7a06 | ||
|
|
b6a4604952 | ||
|
|
af752fbbc2 | ||
|
|
279c9d706a | ||
|
|
806e7b5530 | ||
|
|
f3dc6a217b | ||
|
|
7671d791fa | ||
|
|
8cd84608a5 | ||
|
|
980c6fc810 | ||
|
|
fb40a484c5 | ||
|
|
daa9dedcaa | ||
|
|
0d634345ac | ||
|
|
e648252479 | ||
|
|
179d7a9ad8 | ||
|
|
19bc962ad5 | ||
|
|
27cce086c6 | ||
|
|
fec0c620d4 | ||
|
|
05a1a31cab | ||
|
|
d020527c6f | ||
|
|
4451485664 | ||
|
|
a4e1a3738a | ||
|
|
4339dbeb8d | ||
|
|
5b0605774c | ||
|
|
e3684e25f8 | ||
|
|
1359213196 | ||
|
|
03efc6a169 | ||
|
|
15b5982211 | ||
|
|
0eb3a5d387 | ||
|
|
7f8777389c | ||
|
|
4eb20f10ad | ||
|
|
daa11df558 | ||
|
|
1bb0db30a0 | ||
|
|
02910b0020 | ||
|
|
23b8901c9c | ||
|
|
99f6ed0cd7 | ||
|
|
890c310880 | ||
|
|
0194eeb31f | ||
|
|
f9be4c62b1 | ||
|
|
027e8c18f1 | ||
|
|
4a3bb35a95 | ||
|
|
4bfb0d4494 | ||
|
|
7e0ef03a1e | ||
|
|
f7dbd95a54 | ||
|
|
515ee2290b | ||
|
|
b0c78910bb | ||
|
|
f4ca62b664 | ||
|
|
8eb8043a3d | ||
|
|
3e8541362a | ||
|
|
789724e348 | ||
|
|
5125b9532f | ||
|
|
ebc9de02b0 | ||
|
|
ec788fa491 | ||
|
|
9b5e264574 |
93
README.md
93
README.md
@@ -16,6 +16,11 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [thumbnails](#thumbnails) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
|
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [thumbnails](#thumbnails) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
|
||||||
|
|
||||||
|
|
||||||
|
## breaking changes \o/
|
||||||
|
|
||||||
|
this is the readme for v0.12 which has a different expression for volume permissions (`-v`); see [the v0.11.x readme](https://github.com/9001/copyparty/tree/15b59822112dda56cee576df30f331252fc62628#readme) for stuff regarding the [current stable release](https://github.com/9001/copyparty/releases/tag/v0.11.47)
|
||||||
|
|
||||||
|
|
||||||
## readme toc
|
## readme toc
|
||||||
|
|
||||||
* top
|
* top
|
||||||
@@ -30,11 +35,12 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
* [the browser](#the-browser)
|
* [the browser](#the-browser)
|
||||||
* [tabs](#tabs)
|
* [tabs](#tabs)
|
||||||
* [hotkeys](#hotkeys)
|
* [hotkeys](#hotkeys)
|
||||||
* [tree-mode](#tree-mode)
|
* [navpane](#navpane)
|
||||||
* [thumbnails](#thumbnails)
|
* [thumbnails](#thumbnails)
|
||||||
* [zip downloads](#zip-downloads)
|
* [zip downloads](#zip-downloads)
|
||||||
* [uploading](#uploading)
|
* [uploading](#uploading)
|
||||||
* [file-search](#file-search)
|
* [file-search](#file-search)
|
||||||
|
* [file manager](#file-manager)
|
||||||
* [markdown viewer](#markdown-viewer)
|
* [markdown viewer](#markdown-viewer)
|
||||||
* [other tricks](#other-tricks)
|
* [other tricks](#other-tricks)
|
||||||
* [searching](#searching)
|
* [searching](#searching)
|
||||||
@@ -66,15 +72,14 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
|
|
||||||
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||||
|
|
||||||
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want accounts and volumes etc
|
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want [accounts and volumes](#accounts-and-volumes) etc
|
||||||
|
|
||||||
some recommended options:
|
some recommended options:
|
||||||
* `-e2dsa` enables general file indexing, see [search configuration](#search-configuration)
|
* `-e2dsa` enables general file indexing, see [search configuration](#search-configuration)
|
||||||
* `-e2ts` enables audio metadata indexing (needs either FFprobe or mutagen), see [optional dependencies](#optional-dependencies)
|
* `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen), see [optional dependencies](#optional-dependencies)
|
||||||
* `-v /mnt/music:/music:r:afoo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, with user `foo` as `a`dmin (read/write), password `bar`
|
* `-v /mnt/music:/music:r:rw,foo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, and read-write for user `foo`, password `bar`
|
||||||
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
* replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else
|
||||||
* replace `:r:afoo` with `:rfoo` to only make the folder readable by `foo` and nobody else
|
* see [accounts and volumes](#accounts-and-volumes) for the syntax and other access levels (`r`ead, `w`rite, `m`ove, `d`elete)
|
||||||
* in addition to `r`ead and `a`dmin, `w`rite makes a folder write-only, so cannot list/access files in it
|
|
||||||
* `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access
|
* `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access
|
||||||
|
|
||||||
you may also want these, especially on servers:
|
you may also want these, especially on servers:
|
||||||
@@ -117,7 +122,7 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
* ☑ sanic multipart parser
|
* ☑ sanic multipart parser
|
||||||
* ☑ multiprocessing (actual multithreading)
|
* ☑ multiprocessing (actual multithreading)
|
||||||
* ☑ volumes (mountpoints)
|
* ☑ volumes (mountpoints)
|
||||||
* ☑ accounts
|
* ☑ [accounts](#accounts-and-volumes)
|
||||||
* upload
|
* upload
|
||||||
* ☑ basic: plain multipart, ie6 support
|
* ☑ basic: plain multipart, ie6 support
|
||||||
* ☑ up2k: js, resumable, multithreaded
|
* ☑ up2k: js, resumable, multithreaded
|
||||||
@@ -128,7 +133,7 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
* ☑ folders as zip / tar files
|
* ☑ folders as zip / tar files
|
||||||
* ☑ FUSE client (read-only)
|
* ☑ FUSE client (read-only)
|
||||||
* browser
|
* browser
|
||||||
* ☑ tree-view
|
* ☑ navpane (directory tree sidebar)
|
||||||
* ☑ audio player (with OS media controls)
|
* ☑ audio player (with OS media controls)
|
||||||
* ☑ thumbnails
|
* ☑ thumbnails
|
||||||
* ☑ ...of images using Pillow
|
* ☑ ...of images using Pillow
|
||||||
@@ -136,7 +141,7 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||||
* ☑ image gallery with webm player
|
* ☑ image gallery with webm player
|
||||||
* ☑ SPA (browse while uploading)
|
* ☑ SPA (browse while uploading)
|
||||||
* if you use the file-tree on the left only, not folders in the file list
|
* if you use the navpane to navigate, not folders in the file list
|
||||||
* server indexing
|
* server indexing
|
||||||
* ☑ locate files by contents
|
* ☑ locate files by contents
|
||||||
* ☑ search by name/path/date/size
|
* ☑ search by name/path/date/size
|
||||||
@@ -155,7 +160,7 @@ small collection of user feedback
|
|||||||
|
|
||||||
# bugs
|
# bugs
|
||||||
|
|
||||||
* Windows: python 3.7 and older cannot read tags with ffprobe, so use mutagen or upgrade
|
* Windows: python 3.7 and older cannot read tags with FFprobe, so use Mutagen or upgrade
|
||||||
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
||||||
* Windows: python 2.7 cannot handle filenames with mojibake
|
* Windows: python 2.7 cannot handle filenames with mojibake
|
||||||
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions
|
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions
|
||||||
@@ -164,8 +169,6 @@ small collection of user feedback
|
|||||||
|
|
||||||
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
||||||
* cannot mount something at `/d1/d2/d3` unless `d2` exists inside `d1`
|
* cannot mount something at `/d1/d2/d3` unless `d2` exists inside `d1`
|
||||||
* dupe files will not have metadata (audio tags etc) displayed in the file listing
|
|
||||||
* because they don't get `up` entries in the db (probably best fix) and `tx_browser` does not `lstat`
|
|
||||||
* probably more, pls let me know
|
* probably more, pls let me know
|
||||||
|
|
||||||
## not my bugs
|
## not my bugs
|
||||||
@@ -176,6 +179,33 @@ small collection of user feedback
|
|||||||
* Windows: msys2-python 3.8.6 occasionally throws `RuntimeError: release unlocked lock` when leaving a scoped mutex in up2k
|
* Windows: msys2-python 3.8.6 occasionally throws `RuntimeError: release unlocked lock` when leaving a scoped mutex in up2k
|
||||||
* this is an msys2 bug, the regular windows edition of python is fine
|
* this is an msys2 bug, the regular windows edition of python is fine
|
||||||
|
|
||||||
|
* VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf
|
||||||
|
* use `--hist` or the `hist` volflag (`-v [...]:chist=/tmp/foo`) to place the db inside the vm instead
|
||||||
|
|
||||||
|
|
||||||
|
# accounts and volumes
|
||||||
|
|
||||||
|
* `-a usr:pwd` adds account `usr` with password `pwd`
|
||||||
|
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
|
||||||
|
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||||
|
* when granting permissions to an account, the names are comma-separated: `-v .::r,usr1,usr2:rw,usr3,usr4`
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
* `r` (read): browse folder contents, download files, download as zip/tar
|
||||||
|
* `w` (write): upload files, move files *into* folder
|
||||||
|
* `m` (move): move files/folders *from* folder
|
||||||
|
* `d` (delete): delete files/folders
|
||||||
|
|
||||||
|
example:
|
||||||
|
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
||||||
|
* make folder `/srv` the root of the filesystem, read-only by anyone: `-v /srv::r`
|
||||||
|
* make folder `/mnt/music` available at `/music`, read-only for u1 and u2, read-write for u3: `-v /mnt/music:music:r,u1,u2:rw,u3`
|
||||||
|
* unauthorized users accessing the webroot can see that the `music` folder exists, but cannot open it
|
||||||
|
* make folder `/mnt/incoming` available at `/inc`, write-only for u1, read-move for u2: `-v /mnt/incoming:inc:w,u1:rm,u2`
|
||||||
|
* unauthorized users accessing the webroot can see that the `inc` folder exists, but cannot open it
|
||||||
|
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
|
||||||
|
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
|
||||||
|
|
||||||
|
|
||||||
# the browser
|
# the browser
|
||||||
|
|
||||||
@@ -196,11 +226,20 @@ small collection of user feedback
|
|||||||
## hotkeys
|
## hotkeys
|
||||||
|
|
||||||
the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
||||||
* `B` toggle breadcrumbs / directory tree
|
* `B` toggle breadcrumbs / navpane
|
||||||
* `I/K` prev/next folder
|
* `I/K` prev/next folder
|
||||||
* `M` parent folder (or unexpand current)
|
* `M` parent folder (or unexpand current)
|
||||||
* `G` toggle list / grid view
|
* `G` toggle list / grid view
|
||||||
* `T` toggle thumbnails / icons
|
* `T` toggle thumbnails / icons
|
||||||
|
* `ctrl-X` cut selected files/folders
|
||||||
|
* `ctrl-V` paste
|
||||||
|
* `F2` rename selected file/folder
|
||||||
|
* when a file/folder is selected (in not-grid-view):
|
||||||
|
* `Up/Down` move cursor
|
||||||
|
* shift+`Up/Down` select and move cursor
|
||||||
|
* ctrl+`Up/Down` move cursor and scroll viewport
|
||||||
|
* `Space` toggle file selection
|
||||||
|
* `Ctrl-A` toggle select all
|
||||||
* when playing audio:
|
* when playing audio:
|
||||||
* `J/L` prev/next song
|
* `J/L` prev/next song
|
||||||
* `U/O` skip 10sec back/forward
|
* `U/O` skip 10sec back/forward
|
||||||
@@ -217,7 +256,7 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
|||||||
* `C` continue playing next video
|
* `C` continue playing next video
|
||||||
* `R` loop
|
* `R` loop
|
||||||
* `M` mute
|
* `M` mute
|
||||||
* when tree-sidebar is open:
|
* when the navpane is open:
|
||||||
* `A/D` adjust tree width
|
* `A/D` adjust tree width
|
||||||
* in the grid view:
|
* in the grid view:
|
||||||
* `S` toggle multiselect
|
* `S` toggle multiselect
|
||||||
@@ -230,9 +269,10 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
|||||||
* `^e` toggle editor / preview
|
* `^e` toggle editor / preview
|
||||||
* `^up, ^down` jump paragraphs
|
* `^up, ^down` jump paragraphs
|
||||||
|
|
||||||
## tree-mode
|
|
||||||
|
|
||||||
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the `🌲` or pressing the `B` hotkey
|
## navpane
|
||||||
|
|
||||||
|
by default there's a breadcrumbs path; you can replace this with a navpane (tree-browser sidebar thing) by clicking the `🌲` or pressing the `B` hotkey
|
||||||
|
|
||||||
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
||||||
|
|
||||||
@@ -322,6 +362,13 @@ note that since up2k has to read the file twice, `[🎈 bup]` can be up to 2x fa
|
|||||||
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)
|
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)
|
||||||
|
|
||||||
|
|
||||||
|
## file manager
|
||||||
|
|
||||||
|
if you have the required permissions, you can cut/paste, rename, and delete files/folders
|
||||||
|
|
||||||
|
you can move files across browser tabs (cut in one tab, paste in another)
|
||||||
|
|
||||||
|
|
||||||
## markdown viewer
|
## markdown viewer
|
||||||
|
|
||||||

|

|
||||||
@@ -406,12 +453,12 @@ tags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric val
|
|||||||
|
|
||||||
see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/master/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,)
|
see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/master/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,)
|
||||||
|
|
||||||
`--no-mutagen` disables mutagen and uses ffprobe instead, which...
|
`--no-mutagen` disables Mutagen and uses FFprobe instead, which...
|
||||||
* is about 20x slower than mutagen
|
* is about 20x slower than Mutagen
|
||||||
* catches a few tags that mutagen doesn't
|
* catches a few tags that Mutagen doesn't
|
||||||
* melodic key, video resolution, framerate, pixfmt
|
* melodic key, video resolution, framerate, pixfmt
|
||||||
* avoids pulling any GPL code into copyparty
|
* avoids pulling any GPL code into copyparty
|
||||||
* more importantly runs ffprobe on incoming files which is bad if your ffmpeg has a cve
|
* more importantly runs FFprobe on incoming files which is bad if your FFmpeg has a cve
|
||||||
|
|
||||||
|
|
||||||
## file parser plugins
|
## file parser plugins
|
||||||
@@ -448,7 +495,7 @@ copyparty can invoke external programs to collect additional metadata for files
|
|||||||
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
|
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
|
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| directory tree | - | - | `*1` | yep | yep | yep | yep | yep |
|
| navpane | - | - | `*1` | yep | yep | yep | yep | yep |
|
||||||
| up2k | - | - | yep | yep | yep | yep | yep | yep |
|
| up2k | - | - | yep | yep | yep | yep | yep | yep |
|
||||||
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
||||||
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
||||||
@@ -545,7 +592,7 @@ below are some tweaks roughly ordered by usefulness:
|
|||||||
|
|
||||||
enable music tags:
|
enable music tags:
|
||||||
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
||||||
* or `FFprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
||||||
|
|
||||||
enable thumbnails of images:
|
enable thumbnails of images:
|
||||||
* `Pillow` (requires py2.7 or py3.5+)
|
* `Pillow` (requires py2.7 or py3.5+)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
#
|
#
|
||||||
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
||||||
# accept connections; correctly delaying units depending on copyparty.
|
# accept connections; correctly delaying units depending on copyparty.
|
||||||
# But note that journalctl does not show messages in the correct order
|
# But note that journalctl will get the timestamps wrong due to
|
||||||
# (or with correct timestamps even), so you get confusing stuff like
|
# python disabling line-buffering, so messages are out-of-order:
|
||||||
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
@@ -19,6 +19,7 @@ Description=copyparty file server
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=notify
|
||||||
|
SyslogIdentifier=copyparty
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
|
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
|
||||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
|
|||||||
@@ -199,24 +199,30 @@ def run_argparse(argv, formatter):
|
|||||||
epilog=dedent(
|
epilog=dedent(
|
||||||
"""
|
"""
|
||||||
-a takes username:password,
|
-a takes username:password,
|
||||||
-v takes src:dst:permset:permset:cflag:cflag:...
|
-v takes src:dst:perm1:perm2:permN:cflag1:cflag2:cflagN:...
|
||||||
where "permset" is accesslevel followed by username (no separator)
|
where "perm" is "accesslevels,username1,username2,..."
|
||||||
and "cflag" is config flags to set on this volume
|
and "cflag" is config flags to set on this volume
|
||||||
|
|
||||||
|
list of accesslevels:
|
||||||
|
"r" (read): list folder contents, download files
|
||||||
|
"w" (write): upload files; need "r" to see the uploads
|
||||||
|
"m" (move): move files and folders; need "w" at destination
|
||||||
|
"d" (delete): permanently delete files and folders
|
||||||
|
|
||||||
list of cflags:
|
list of cflags:
|
||||||
"cnodupe" rejects existing files (instead of symlinking them)
|
"c,nodupe" rejects existing files (instead of symlinking them)
|
||||||
"ce2d" sets -e2d (all -e2* args can be set using ce2* cflags)
|
"c,e2d" sets -e2d (all -e2* args can be set using ce2* cflags)
|
||||||
"cd2t" disables metadata collection, overrides -e2t*
|
"c,d2t" disables metadata collection, overrides -e2t*
|
||||||
"cd2d" disables all database stuff, overrides -e2*
|
"c,d2d" disables all database stuff, overrides -e2*
|
||||||
|
|
||||||
example:\033[35m
|
example:\033[35m
|
||||||
-a ed:hunter2 -v .::r:aed -v ../inc:dump:w:aed:cnodupe \033[36m
|
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
|
||||||
mount current directory at "/" with
|
mount current directory at "/" with
|
||||||
* r (read-only) for everyone
|
* r (read-only) for everyone
|
||||||
* a (read+write) for ed
|
* rw (read+write) for ed
|
||||||
mount ../inc at "/dump" with
|
mount ../inc at "/dump" with
|
||||||
* w (write-only) for everyone
|
* w (write-only) for everyone
|
||||||
* a (read+write) for ed
|
* rw (read+write) for ed
|
||||||
* reject duplicate files \033[0m
|
* reject duplicate files \033[0m
|
||||||
|
|
||||||
if no accounts or volumes are configured,
|
if no accounts or volumes are configured,
|
||||||
@@ -277,6 +283,8 @@ def run_argparse(argv, formatter):
|
|||||||
|
|
||||||
ap2 = ap.add_argument_group('opt-outs')
|
ap2 = ap.add_argument_group('opt-outs')
|
||||||
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
||||||
|
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
|
||||||
|
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
|
||||||
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
||||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||||
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")
|
||||||
@@ -288,6 +296,7 @@ def run_argparse(argv, formatter):
|
|||||||
ap2 = ap.add_argument_group('logging options')
|
ap2 = ap.add_argument_group('logging options')
|
||||||
ap2.add_argument("-q", action="store_true", help="quiet")
|
ap2.add_argument("-q", action="store_true", help="quiet")
|
||||||
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
|
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
|
||||||
|
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
|
||||||
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
|
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
|
||||||
ap2.add_argument("--log-htp", action="store_true", help="print http-server threadpool scaling")
|
ap2.add_argument("--log-htp", action="store_true", help="print http-server threadpool scaling")
|
||||||
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
|
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
|
||||||
@@ -319,8 +328,11 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
||||||
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume state")
|
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume state")
|
||||||
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||||
ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead")
|
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead")
|
||||||
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
||||||
|
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader")
|
||||||
|
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
|
||||||
|
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval (0=off)")
|
||||||
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
|
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
|
||||||
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
|
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
|
||||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,ac,vc,res,.fps")
|
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,ac,vc,res,.fps")
|
||||||
@@ -376,6 +388,36 @@ def main(argv=None):
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
al = run_argparse(argv, Dodge11874)
|
al = run_argparse(argv, Dodge11874)
|
||||||
|
|
||||||
|
nstrs = []
|
||||||
|
anymod = False
|
||||||
|
for ostr in al.v or []:
|
||||||
|
mod = False
|
||||||
|
oa = ostr.split(":")
|
||||||
|
na = oa[:2]
|
||||||
|
for opt in oa[2:]:
|
||||||
|
if re.match("c[^,]", opt):
|
||||||
|
mod = True
|
||||||
|
na.append("c," + opt[1:])
|
||||||
|
elif re.sub("^[rwmd]*", "", opt) and "," not in opt:
|
||||||
|
mod = True
|
||||||
|
perm = opt[0]
|
||||||
|
if perm == "a":
|
||||||
|
perm = "rw"
|
||||||
|
na.append(perm + "," + opt[1:])
|
||||||
|
else:
|
||||||
|
na.append(opt)
|
||||||
|
|
||||||
|
nstr = ":".join(na)
|
||||||
|
nstrs.append(nstr if mod else ostr)
|
||||||
|
if mod:
|
||||||
|
msg = "\033[1;31mWARNING:\033[0;1m\n -v {} \033[0;33mwas replaced with\033[0;1m\n -v {} \n\033[0m"
|
||||||
|
lprint(msg.format(ostr, nstr))
|
||||||
|
anymod = True
|
||||||
|
|
||||||
|
if anymod:
|
||||||
|
al.v = nstrs
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
# propagate implications
|
# propagate implications
|
||||||
for k1, k2 in IMPLICATIONS:
|
for k1, k2 in IMPLICATIONS:
|
||||||
if getattr(al, k1):
|
if getattr(al, k1):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 11, 41)
|
VERSION = (0, 12, 1)
|
||||||
CODENAME = "the grid"
|
CODENAME = "fil\033[33med"
|
||||||
BUILD_DT = (2021, 7, 17)
|
BUILD_DT = (2021, 7, 28)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -10,20 +10,35 @@ import hashlib
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .__init__ import WINDOWS
|
from .__init__ import WINDOWS
|
||||||
from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir
|
from .util import IMPLICATIONS, uncyg, undot, absreal, Pebkac, fsdec, fsenc, statdir
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
|
class AXS(object):
|
||||||
|
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
||||||
|
self.uread = {} if uread is None else {k: 1 for k in uread}
|
||||||
|
self.uwrite = {} if uwrite is None else {k: 1 for k in uwrite}
|
||||||
|
self.umove = {} if umove is None else {k: 1 for k in umove}
|
||||||
|
self.udel = {} if udel is None else {k: 1 for k in udel}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "AXS({})".format(
|
||||||
|
", ".join(
|
||||||
|
"{}={!r}".format(k, self.__dict__[k])
|
||||||
|
for k in "uread uwrite umove udel".split()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VFS(object):
|
class VFS(object):
|
||||||
"""single level in the virtual fs"""
|
"""single level in the virtual fs"""
|
||||||
|
|
||||||
def __init__(self, log, realpath, vpath, uread, uwrite, uadm, flags):
|
def __init__(self, log, realpath, vpath, axs, flags):
|
||||||
self.log = log
|
self.log = log
|
||||||
self.realpath = realpath # absolute path on host filesystem
|
self.realpath = realpath # absolute path on host filesystem
|
||||||
self.vpath = vpath # absolute path in the virtual filesystem
|
self.vpath = vpath # absolute path in the virtual filesystem
|
||||||
self.uread = uread # users who can read this
|
self.axs = axs # type: AXS
|
||||||
self.uwrite = uwrite # users who can write this
|
self.flags = flags # config options
|
||||||
self.uadm = uadm # users who are regular admins
|
|
||||||
self.flags = flags # config switches
|
|
||||||
self.nodes = {} # child nodes
|
self.nodes = {} # child nodes
|
||||||
self.histtab = None # all realpath->histpath
|
self.histtab = None # all realpath->histpath
|
||||||
self.dbv = None # closest full/non-jump parent
|
self.dbv = None # closest full/non-jump parent
|
||||||
@@ -31,15 +46,23 @@ class VFS(object):
|
|||||||
if realpath:
|
if realpath:
|
||||||
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||||
self.all_vols = {vpath: self} # flattened recursive
|
self.all_vols = {vpath: self} # flattened recursive
|
||||||
|
self.aread = {}
|
||||||
|
self.awrite = {}
|
||||||
|
self.amove = {}
|
||||||
|
self.adel = {}
|
||||||
else:
|
else:
|
||||||
self.histpath = None
|
self.histpath = None
|
||||||
self.all_vols = None
|
self.all_vols = None
|
||||||
|
self.aread = None
|
||||||
|
self.awrite = None
|
||||||
|
self.amove = None
|
||||||
|
self.adel = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "VFS({})".format(
|
return "VFS({})".format(
|
||||||
", ".join(
|
", ".join(
|
||||||
"{}={!r}".format(k, self.__dict__[k])
|
"{}={!r}".format(k, self.__dict__[k])
|
||||||
for k in "realpath vpath uread uwrite uadm flags".split()
|
for k in "realpath vpath axs flags".split()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,9 +89,7 @@ class VFS(object):
|
|||||||
self.log,
|
self.log,
|
||||||
os.path.join(self.realpath, name) if self.realpath else None,
|
os.path.join(self.realpath, name) if self.realpath else None,
|
||||||
"{}/{}".format(self.vpath, name).lstrip("/"),
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
||||||
self.uread,
|
self.axs,
|
||||||
self.uwrite,
|
|
||||||
self.uadm,
|
|
||||||
self._copy_flags(name),
|
self._copy_flags(name),
|
||||||
)
|
)
|
||||||
vn.dbv = self.dbv or self
|
vn.dbv = self.dbv or self
|
||||||
@@ -81,7 +102,7 @@ class VFS(object):
|
|||||||
|
|
||||||
# leaf does not exist; create and keep permissions blank
|
# leaf does not exist; create and keep permissions blank
|
||||||
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
||||||
vn = VFS(self.log, src, vp, [], [], [], {})
|
vn = VFS(self.log, src, vp, AXS(), {})
|
||||||
vn.dbv = self.dbv or self
|
vn.dbv = self.dbv or self
|
||||||
self.nodes[dst] = vn
|
self.nodes[dst] = vn
|
||||||
return vn
|
return vn
|
||||||
@@ -121,23 +142,32 @@ class VFS(object):
|
|||||||
return [self, vpath]
|
return [self, vpath]
|
||||||
|
|
||||||
def can_access(self, vpath, uname):
|
def can_access(self, vpath, uname):
|
||||||
"""return [readable,writable]"""
|
# type: (str, str) -> tuple[bool, bool, bool, bool]
|
||||||
|
"""can Read,Write,Move,Delete"""
|
||||||
vn, _ = self._find(vpath)
|
vn, _ = self._find(vpath)
|
||||||
|
c = vn.axs
|
||||||
return [
|
return [
|
||||||
uname in vn.uread or "*" in vn.uread,
|
uname in c.uread or "*" in c.uread,
|
||||||
uname in vn.uwrite or "*" in vn.uwrite,
|
uname in c.uwrite or "*" in c.uwrite,
|
||||||
|
uname in c.umove or "*" in c.umove,
|
||||||
|
uname in c.udel or "*" in c.udel,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get(self, vpath, uname, will_read, will_write):
|
def get(self, vpath, uname, will_read, will_write, will_move=False, will_del=False):
|
||||||
# type: (str, str, bool, bool) -> tuple[VFS, str]
|
# type: (str, str, bool, bool, bool, bool) -> tuple[VFS, str]
|
||||||
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
||||||
vn, rem = self._find(vpath)
|
vn, rem = self._find(vpath)
|
||||||
|
c = vn.axs
|
||||||
|
|
||||||
if will_read and (uname not in vn.uread and "*" not in vn.uread):
|
for req, d, msg in [
|
||||||
raise Pebkac(403, "you don't have read-access for this location")
|
[will_read, c.uread, "read"],
|
||||||
|
[will_write, c.uwrite, "write"],
|
||||||
if will_write and (uname not in vn.uwrite and "*" not in vn.uwrite):
|
[will_move, c.umove, "move"],
|
||||||
raise Pebkac(403, "you don't have write-access for this location")
|
[will_del, c.udel, "delete"],
|
||||||
|
]:
|
||||||
|
if req and (uname not in d and "*" not in d):
|
||||||
|
m = "you don't have {}-access for this location"
|
||||||
|
raise Pebkac(403, m.format(msg))
|
||||||
|
|
||||||
return vn, rem
|
return vn, rem
|
||||||
|
|
||||||
@@ -150,65 +180,50 @@ class VFS(object):
|
|||||||
vrem = "/".join([x for x in vrem if x])
|
vrem = "/".join([x for x in vrem if x])
|
||||||
return dbv, vrem
|
return dbv, vrem
|
||||||
|
|
||||||
def canonical(self, rem):
|
def canonical(self, rem, resolve=True):
|
||||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||||
rp = self.realpath
|
rp = self.realpath
|
||||||
if rem:
|
if rem:
|
||||||
rp += "/" + rem
|
rp += "/" + rem
|
||||||
|
|
||||||
try:
|
return absreal(rp) if resolve else rp
|
||||||
return fsdec(os.path.realpath(fsenc(rp)))
|
|
||||||
except:
|
|
||||||
if not WINDOWS:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# cpython bug introduced in 3.8, still exists in 3.9.1;
|
def ls(self, rem, uname, scandir, permsets, lstat=False):
|
||||||
# some win7sp1 and win10:20H2 boxes cannot realpath a
|
# type: (str, str, bool, list[list[bool]], bool) -> tuple[str, str, dict[str, VFS]]
|
||||||
# networked drive letter such as b"n:" or b"n:\\"
|
|
||||||
#
|
|
||||||
# requirements to trigger:
|
|
||||||
# * bytestring (not unicode str)
|
|
||||||
# * just the drive letter (subfolders are ok)
|
|
||||||
# * networked drive (regular disks and vmhgfs are ok)
|
|
||||||
# * on an enterprise network (idk, cannot repro with samba)
|
|
||||||
#
|
|
||||||
# hits the following exceptions in succession:
|
|
||||||
# * access denied at L601: "path = _getfinalpathname(path)"
|
|
||||||
# * "cant concat str to bytes" at L621: "return path + tail"
|
|
||||||
#
|
|
||||||
return os.path.realpath(rp)
|
|
||||||
|
|
||||||
def ls(self, rem, uname, scandir, incl_wo=False, lstat=False):
|
|
||||||
# type: (str, str, bool, bool, bool) -> tuple[str, str, dict[str, VFS]]
|
|
||||||
"""return user-readable [fsdir,real,virt] items at vpath"""
|
"""return user-readable [fsdir,real,virt] items at vpath"""
|
||||||
virt_vis = {} # nodes readable by user
|
virt_vis = {} # nodes readable by user
|
||||||
abspath = self.canonical(rem)
|
abspath = self.canonical(rem)
|
||||||
real = list(statdir(self.log, scandir, lstat, abspath))
|
real = list(statdir(self.log, scandir, lstat, abspath))
|
||||||
real.sort()
|
real.sort()
|
||||||
if not rem:
|
if not rem:
|
||||||
for name, vn2 in sorted(self.nodes.items()):
|
# no vfs nodes in the list of real inodes
|
||||||
ok = uname in vn2.uread or "*" in vn2.uread
|
real = [x for x in real if x[0] not in self.nodes]
|
||||||
|
|
||||||
if not ok and incl_wo:
|
for name, vn2 in sorted(self.nodes.items()):
|
||||||
ok = uname in vn2.uwrite or "*" in vn2.uwrite
|
ok = False
|
||||||
|
axs = vn2.axs
|
||||||
|
axs = [axs.uread, axs.uwrite, axs.umove, axs.udel]
|
||||||
|
for pset in permsets:
|
||||||
|
ok = True
|
||||||
|
for req, lst in zip(pset, axs):
|
||||||
|
if req and uname not in lst and "*" not in lst:
|
||||||
|
ok = False
|
||||||
|
if ok:
|
||||||
|
break
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
virt_vis[name] = vn2
|
virt_vis[name] = vn2
|
||||||
|
|
||||||
# no vfs nodes in the list of real inodes
|
|
||||||
real = [x for x in real if x[0] not in self.nodes]
|
|
||||||
|
|
||||||
return [abspath, real, virt_vis]
|
return [abspath, real, virt_vis]
|
||||||
|
|
||||||
def walk(self, rel, rem, seen, uname, dots, scandir, lstat):
|
def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat):
|
||||||
"""
|
"""
|
||||||
recursively yields from ./rem;
|
recursively yields from ./rem;
|
||||||
rel is a unix-style user-defined vpath (not vfs-related)
|
rel is a unix-style user-defined vpath (not vfs-related)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fsroot, vfs_ls, vfs_virt = self.ls(
|
fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat)
|
||||||
rem, uname, scandir, incl_wo=False, lstat=lstat
|
dbv, vrem = self.get_dbv(rem)
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
seen
|
seen
|
||||||
@@ -226,7 +241,7 @@ class VFS(object):
|
|||||||
rfiles.sort()
|
rfiles.sort()
|
||||||
rdirs.sort()
|
rdirs.sort()
|
||||||
|
|
||||||
yield rel, fsroot, rfiles, rdirs, vfs_virt
|
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
|
||||||
|
|
||||||
for rdir, _ in rdirs:
|
for rdir, _ in rdirs:
|
||||||
if not dots and rdir.startswith("."):
|
if not dots and rdir.startswith("."):
|
||||||
@@ -234,7 +249,7 @@ class VFS(object):
|
|||||||
|
|
||||||
wrel = (rel + "/" + rdir).lstrip("/")
|
wrel = (rel + "/" + rdir).lstrip("/")
|
||||||
wrem = (rem + "/" + rdir).lstrip("/")
|
wrem = (rem + "/" + rdir).lstrip("/")
|
||||||
for x in self.walk(wrel, wrem, seen, uname, dots, scandir, lstat):
|
for x in self.walk(wrel, wrem, seen, uname, permsets, dots, scandir, lstat):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
for n, vfs in sorted(vfs_virt.items()):
|
for n, vfs in sorted(vfs_virt.items()):
|
||||||
@@ -242,7 +257,7 @@ class VFS(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
wrel = (rel + "/" + n).lstrip("/")
|
wrel = (rel + "/" + n).lstrip("/")
|
||||||
for x in vfs.walk(wrel, "", seen, uname, dots, scandir, lstat):
|
for x in vfs.walk(wrel, "", seen, uname, permsets, dots, scandir, lstat):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
def zipgen(self, vrem, flt, uname, dots, scandir):
|
def zipgen(self, vrem, flt, uname, dots, scandir):
|
||||||
@@ -253,9 +268,8 @@ class VFS(object):
|
|||||||
f2a = os.sep + "dir.txt"
|
f2a = os.sep + "dir.txt"
|
||||||
f2b = "{0}.hist{0}".format(os.sep)
|
f2b = "{0}.hist{0}".format(os.sep)
|
||||||
|
|
||||||
for vpath, apath, files, rd, vd in self.walk(
|
g = self.walk("", vrem, [], uname, [[True]], dots, scandir, False)
|
||||||
"", vrem, [], uname, dots, scandir, False
|
for _, _, vpath, apath, files, rd, vd in g:
|
||||||
):
|
|
||||||
if flt:
|
if flt:
|
||||||
files = [x for x in files if x[0] in flt]
|
files = [x for x in files if x[0] in flt]
|
||||||
|
|
||||||
@@ -295,20 +309,6 @@ class VFS(object):
|
|||||||
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
||||||
yield f
|
yield f
|
||||||
|
|
||||||
def user_tree(self, uname, readable, writable, admin):
|
|
||||||
is_readable = False
|
|
||||||
if uname in self.uread or "*" in self.uread:
|
|
||||||
readable.append(self.vpath)
|
|
||||||
is_readable = True
|
|
||||||
|
|
||||||
if uname in self.uwrite or "*" in self.uwrite:
|
|
||||||
writable.append(self.vpath)
|
|
||||||
if is_readable:
|
|
||||||
admin.append(self.vpath)
|
|
||||||
|
|
||||||
for _, vn in sorted(self.nodes.items()):
|
|
||||||
vn.user_tree(uname, readable, writable, admin)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSrv(object):
|
class AuthSrv(object):
|
||||||
"""verifies users against given paths"""
|
"""verifies users against given paths"""
|
||||||
@@ -341,7 +341,8 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
yield prev, True
|
yield prev, True
|
||||||
|
|
||||||
def _parse_config_file(self, fd, user, mread, mwrite, madm, mflags, mount):
|
def _parse_config_file(self, fd, acct, daxs, mflags, mount):
|
||||||
|
# type: (any, str, dict[str, AXS], any, str) -> None
|
||||||
vol_src = None
|
vol_src = None
|
||||||
vol_dst = None
|
vol_dst = None
|
||||||
self.line_ctr = 0
|
self.line_ctr = 0
|
||||||
@@ -357,7 +358,7 @@ class AuthSrv(object):
|
|||||||
if vol_src is None:
|
if vol_src is None:
|
||||||
if ln.startswith("u "):
|
if ln.startswith("u "):
|
||||||
u, p = ln[2:].split(":", 1)
|
u, p = ln[2:].split(":", 1)
|
||||||
user[u] = p
|
acct[u] = p
|
||||||
else:
|
else:
|
||||||
vol_src = ln
|
vol_src = ln
|
||||||
continue
|
continue
|
||||||
@@ -368,50 +369,49 @@ class AuthSrv(object):
|
|||||||
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
||||||
|
|
||||||
# cfg files override arguments and previous files
|
# cfg files override arguments and previous files
|
||||||
vol_src = fsdec(os.path.abspath(fsenc(vol_src)))
|
vol_src = bos.path.abspath(vol_src)
|
||||||
vol_dst = vol_dst.strip("/")
|
vol_dst = vol_dst.strip("/")
|
||||||
mount[vol_dst] = vol_src
|
mount[vol_dst] = vol_src
|
||||||
mread[vol_dst] = []
|
daxs[vol_dst] = AXS()
|
||||||
mwrite[vol_dst] = []
|
|
||||||
madm[vol_dst] = []
|
|
||||||
mflags[vol_dst] = {}
|
mflags[vol_dst] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(ln) > 1:
|
try:
|
||||||
lvl, uname = ln.split(" ")
|
lvl, uname = ln.split(" ", 1)
|
||||||
else:
|
except:
|
||||||
lvl = ln
|
lvl = ln
|
||||||
uname = "*"
|
uname = "*"
|
||||||
|
|
||||||
self._read_vol_str(
|
if lvl == "a":
|
||||||
lvl,
|
m = "WARNING (config-file): permission flag 'a' is deprecated; please use 'rw' instead"
|
||||||
uname,
|
self.log(m, 1)
|
||||||
mread[vol_dst],
|
|
||||||
mwrite[vol_dst],
|
|
||||||
madm[vol_dst],
|
|
||||||
mflags[vol_dst],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _read_vol_str(self, lvl, uname, mr, mw, ma, mf):
|
self._read_vol_str(lvl, uname, daxs[vol_dst], mflags[vol_dst])
|
||||||
|
|
||||||
|
def _read_vol_str(self, lvl, uname, axs, flags):
|
||||||
|
# type: (str, str, AXS, any) -> None
|
||||||
if lvl == "c":
|
if lvl == "c":
|
||||||
cval = True
|
cval = True
|
||||||
if "=" in uname:
|
if "=" in uname:
|
||||||
uname, cval = uname.split("=", 1)
|
uname, cval = uname.split("=", 1)
|
||||||
|
|
||||||
self._read_volflag(mf, uname, cval, False)
|
self._read_volflag(flags, uname, cval, False)
|
||||||
return
|
return
|
||||||
|
|
||||||
if uname == "":
|
if uname == "":
|
||||||
uname = "*"
|
uname = "*"
|
||||||
|
|
||||||
if lvl in "ra":
|
if "r" in lvl:
|
||||||
mr.append(uname)
|
axs.uread[uname] = 1
|
||||||
|
|
||||||
if lvl in "wa":
|
if "w" in lvl:
|
||||||
mw.append(uname)
|
axs.uwrite[uname] = 1
|
||||||
|
|
||||||
if lvl == "a":
|
if "m" in lvl:
|
||||||
ma.append(uname)
|
axs.umove[uname] = 1
|
||||||
|
|
||||||
|
if "d" in lvl:
|
||||||
|
axs.udel[uname] = 1
|
||||||
|
|
||||||
def _read_volflag(self, flags, name, value, is_list):
|
def _read_volflag(self, flags, name, value, is_list):
|
||||||
if name not in ["mtp"]:
|
if name not in ["mtp"]:
|
||||||
@@ -433,21 +433,24 @@ class AuthSrv(object):
|
|||||||
before finally building the VFS
|
before finally building the VFS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = {} # username:password
|
acct = {} # username:password
|
||||||
mread = {} # mountpoint:[username]
|
daxs = {} # type: dict[str, AXS]
|
||||||
mwrite = {} # mountpoint:[username]
|
|
||||||
madm = {} # mountpoint:[username]
|
|
||||||
mflags = {} # mountpoint:[flag]
|
mflags = {} # mountpoint:[flag]
|
||||||
mount = {} # dst:src (mountpoint:realpath)
|
mount = {} # dst:src (mountpoint:realpath)
|
||||||
|
|
||||||
if self.args.a:
|
if self.args.a:
|
||||||
# list of username:password
|
# list of username:password
|
||||||
for u, p in [x.split(":", 1) for x in self.args.a]:
|
for x in self.args.a:
|
||||||
user[u] = p
|
try:
|
||||||
|
u, p = x.split(":", 1)
|
||||||
|
acct[u] = p
|
||||||
|
except:
|
||||||
|
m = '\n invalid value "{}" for argument -a, must be username:password'
|
||||||
|
raise Exception(m.format(x))
|
||||||
|
|
||||||
if self.args.v:
|
if self.args.v:
|
||||||
# list of src:dst:permset:permset:...
|
# list of src:dst:permset:permset:...
|
||||||
# permset is [rwa]username or [c]flag
|
# permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
|
||||||
for v_str in self.args.v:
|
for v_str in self.args.v:
|
||||||
m = self.re_vol.match(v_str)
|
m = self.re_vol.match(v_str)
|
||||||
if not m:
|
if not m:
|
||||||
@@ -458,27 +461,21 @@ class AuthSrv(object):
|
|||||||
src = uncyg(src)
|
src = uncyg(src)
|
||||||
|
|
||||||
# print("\n".join([src, dst, perms]))
|
# print("\n".join([src, dst, perms]))
|
||||||
src = fsdec(os.path.abspath(fsenc(src)))
|
src = bos.path.abspath(src)
|
||||||
dst = dst.strip("/")
|
dst = dst.strip("/")
|
||||||
mount[dst] = src
|
mount[dst] = src
|
||||||
mread[dst] = []
|
daxs[dst] = AXS()
|
||||||
mwrite[dst] = []
|
|
||||||
madm[dst] = []
|
|
||||||
mflags[dst] = {}
|
mflags[dst] = {}
|
||||||
|
|
||||||
perms = perms.split(":")
|
for x in perms.split(":"):
|
||||||
for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
|
lvl, uname = x.split(",", 1) if "," in x else [x, ""]
|
||||||
self._read_vol_str(
|
self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])
|
||||||
lvl, uname, mread[dst], mwrite[dst], madm[dst], mflags[dst]
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.args.c:
|
if self.args.c:
|
||||||
for cfg_fn in self.args.c:
|
for cfg_fn in self.args.c:
|
||||||
with open(cfg_fn, "rb") as f:
|
with open(cfg_fn, "rb") as f:
|
||||||
try:
|
try:
|
||||||
self._parse_config_file(
|
self._parse_config_file(f, acct, daxs, mflags, mount)
|
||||||
f, user, mread, mwrite, madm, mflags, mount
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
|
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
|
||||||
self.log(m.format(cfg_fn, self.line_ctr), 1)
|
self.log(m.format(cfg_fn, self.line_ctr), 1)
|
||||||
@@ -488,19 +485,17 @@ class AuthSrv(object):
|
|||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
cased = {}
|
cased = {}
|
||||||
for k, v in mount.items():
|
for k, v in mount.items():
|
||||||
try:
|
cased[k] = absreal(v)
|
||||||
cased[k] = fsdec(os.path.realpath(fsenc(v)))
|
|
||||||
except:
|
|
||||||
cased[k] = v
|
|
||||||
|
|
||||||
mount = cased
|
mount = cased
|
||||||
|
|
||||||
if not mount:
|
if not mount:
|
||||||
# -h says our defaults are CWD at root and read/write for everyone
|
# -h says our defaults are CWD at root and read/write for everyone
|
||||||
vfs = VFS(self.log_func, os.path.abspath("."), "", ["*"], ["*"], ["*"], {})
|
axs = AXS(["*"], ["*"], None, None)
|
||||||
|
vfs = VFS(self.log_func, bos.path.abspath("."), "", 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, None, "", [], [], [], {})
|
vfs = VFS(self.log_func, None, "", AXS(), {})
|
||||||
vfs.flags["d2d"] = True
|
vfs.flags["d2d"] = True
|
||||||
|
|
||||||
maxdepth = 0
|
maxdepth = 0
|
||||||
@@ -511,32 +506,34 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
if dst == "":
|
if dst == "":
|
||||||
# rootfs was mapped; fully replaces the default CWD vfs
|
# rootfs was mapped; fully replaces the default CWD vfs
|
||||||
vfs = VFS(
|
vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
|
||||||
self.log_func,
|
|
||||||
mount[dst],
|
|
||||||
dst,
|
|
||||||
mread[dst],
|
|
||||||
mwrite[dst],
|
|
||||||
madm[dst],
|
|
||||||
mflags[dst],
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
v = vfs.add(mount[dst], dst)
|
v = vfs.add(mount[dst], dst)
|
||||||
v.uread = mread[dst]
|
v.axs = daxs[dst]
|
||||||
v.uwrite = mwrite[dst]
|
|
||||||
v.uadm = madm[dst]
|
|
||||||
v.flags = mflags[dst]
|
v.flags = mflags[dst]
|
||||||
v.dbv = None
|
v.dbv = None
|
||||||
|
|
||||||
vfs.all_vols = {}
|
vfs.all_vols = {}
|
||||||
vfs.get_all_vols(vfs.all_vols)
|
vfs.get_all_vols(vfs.all_vols)
|
||||||
|
|
||||||
|
for perm in "read write move del".split():
|
||||||
|
axs_key = "u" + perm
|
||||||
|
unames = ["*"] + list(acct.keys())
|
||||||
|
umap = {x: [] for x in unames}
|
||||||
|
for usr in unames:
|
||||||
|
for mp, vol in vfs.all_vols.items():
|
||||||
|
if usr in getattr(vol.axs, axs_key):
|
||||||
|
umap[usr].append(mp)
|
||||||
|
setattr(vfs, "a" + perm, umap)
|
||||||
|
|
||||||
|
all_users = {}
|
||||||
missing_users = {}
|
missing_users = {}
|
||||||
for d in [mread, mwrite]:
|
for axs in daxs.values():
|
||||||
for _, ul in d.items():
|
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel]:
|
||||||
for usr in ul:
|
for usr in d.keys():
|
||||||
if usr != "*" and usr not in user:
|
all_users[usr] = 1
|
||||||
|
if usr != "*" and usr not in acct:
|
||||||
missing_users[usr] = 1
|
missing_users[usr] = 1
|
||||||
|
|
||||||
if missing_users:
|
if missing_users:
|
||||||
@@ -560,10 +557,7 @@ class AuthSrv(object):
|
|||||||
elif self.args.hist:
|
elif self.args.hist:
|
||||||
for nch in range(len(hid)):
|
for nch in range(len(hid)):
|
||||||
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
||||||
try:
|
bos.makedirs(hpath)
|
||||||
os.makedirs(hpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
powner = os.path.join(hpath, "owner.txt")
|
powner = os.path.join(hpath, "owner.txt")
|
||||||
try:
|
try:
|
||||||
@@ -583,9 +577,9 @@ class AuthSrv(object):
|
|||||||
vol.histpath = hpath
|
vol.histpath = hpath
|
||||||
break
|
break
|
||||||
|
|
||||||
vol.histpath = os.path.realpath(vol.histpath)
|
vol.histpath = absreal(vol.histpath)
|
||||||
if vol.dbv:
|
if vol.dbv:
|
||||||
if os.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
if bos.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
||||||
promote.append(vol)
|
promote.append(vol)
|
||||||
vol.dbv = None
|
vol.dbv = None
|
||||||
else:
|
else:
|
||||||
@@ -611,7 +605,7 @@ class AuthSrv(object):
|
|||||||
all_mte = {}
|
all_mte = {}
|
||||||
errors = False
|
errors = False
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
if (self.args.e2ds and vol.uwrite) or self.args.e2dsa:
|
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
|
||||||
vol.flags["e2ds"] = True
|
vol.flags["e2ds"] = True
|
||||||
|
|
||||||
if self.args.e2d or "e2ds" in vol.flags:
|
if self.args.e2d or "e2ds" in vol.flags:
|
||||||
@@ -700,6 +694,27 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
vfs.bubble_flags()
|
vfs.bubble_flags()
|
||||||
|
|
||||||
|
m = "volumes and permissions:\n"
|
||||||
|
for v in vfs.all_vols.values():
|
||||||
|
if not self.warn_anonwrite:
|
||||||
|
break
|
||||||
|
|
||||||
|
m += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(v.vpath, v.realpath)
|
||||||
|
for txt, attr in [
|
||||||
|
[" read", "uread"],
|
||||||
|
[" write", "uwrite"],
|
||||||
|
[" move", "umove"],
|
||||||
|
["delete", "udel"],
|
||||||
|
]:
|
||||||
|
u = list(sorted(getattr(v.axs, attr).keys()))
|
||||||
|
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
|
||||||
|
u = u if u else "\033[36m--none--\033[0m"
|
||||||
|
m += "\n| {}: {}".format(txt, u)
|
||||||
|
m += "\n"
|
||||||
|
|
||||||
|
if self.warn_anonwrite and not self.args.no_voldump:
|
||||||
|
self.log(m)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v, _ = vfs.get("/", "*", False, True)
|
v, _ = vfs.get("/", "*", False, True)
|
||||||
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
||||||
@@ -711,17 +726,14 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.vfs = vfs
|
self.vfs = vfs
|
||||||
self.user = user
|
self.acct = acct
|
||||||
self.iuser = {v: k for k, v in user.items()}
|
self.iacct = {v: k for k, v in acct.items()}
|
||||||
|
|
||||||
self.re_pwd = None
|
self.re_pwd = None
|
||||||
pwds = [re.escape(x) for x in self.iuser.keys()]
|
pwds = [re.escape(x) for x in self.iacct.keys()]
|
||||||
if pwds:
|
if pwds:
|
||||||
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
||||||
|
|
||||||
# import pprint
|
|
||||||
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})
|
|
||||||
|
|
||||||
def dbg_ls(self):
|
def dbg_ls(self):
|
||||||
users = self.args.ls
|
users = self.args.ls
|
||||||
vols = "*"
|
vols = "*"
|
||||||
@@ -739,12 +751,12 @@ class AuthSrv(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if users == "**":
|
if users == "**":
|
||||||
users = list(self.user.keys()) + ["*"]
|
users = list(self.acct.keys()) + ["*"]
|
||||||
else:
|
else:
|
||||||
users = [users]
|
users = [users]
|
||||||
|
|
||||||
for u in users:
|
for u in users:
|
||||||
if u not in self.user and u != "*":
|
if u not in self.acct and u != "*":
|
||||||
raise Exception("user not found: " + u)
|
raise Exception("user not found: " + u)
|
||||||
|
|
||||||
if vols == "*":
|
if vols == "*":
|
||||||
@@ -760,8 +772,10 @@ class AuthSrv(object):
|
|||||||
raise Exception("volume not found: " + v)
|
raise Exception("volume not found: " + v)
|
||||||
|
|
||||||
self.log({"users": users, "vols": vols, "flags": flags})
|
self.log({"users": users, "vols": vols, "flags": flags})
|
||||||
|
m = "/{}: read({}) write({}) move({}) del({})"
|
||||||
for k, v in self.vfs.all_vols.items():
|
for k, v in self.vfs.all_vols.items():
|
||||||
self.log("/{}: read({}) write({})".format(k, v.uread, v.uwrite))
|
vc = v.axs
|
||||||
|
self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel))
|
||||||
|
|
||||||
flag_v = "v" in flags
|
flag_v = "v" in flags
|
||||||
flag_ln = "ln" in flags
|
flag_ln = "ln" in flags
|
||||||
@@ -775,13 +789,15 @@ class AuthSrv(object):
|
|||||||
for u in users:
|
for u in users:
|
||||||
self.log("checking /{} as {}".format(v, u))
|
self.log("checking /{} as {}".format(v, u))
|
||||||
try:
|
try:
|
||||||
vn, _ = self.vfs.get(v, u, True, False)
|
vn, _ = self.vfs.get(v, u, True, False, False, False)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
atop = vn.realpath
|
atop = vn.realpath
|
||||||
g = vn.walk("", "", [], u, True, not self.args.no_scandir, False)
|
g = vn.walk(
|
||||||
for vpath, apath, files, _, _ in g:
|
"", "", [], u, True, [[True]], not self.args.no_scandir, False
|
||||||
|
)
|
||||||
|
for _, _, vpath, apath, files, _, _ in g:
|
||||||
fnames = [n[0] for n in files]
|
fnames = [n[0] for n in files]
|
||||||
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
||||||
vpaths = [vtop + x for x in vpaths]
|
vpaths = [vtop + x for x in vpaths]
|
||||||
|
|||||||
0
copyparty/bos/__init__.py
Normal file
0
copyparty/bos/__init__.py
Normal file
59
copyparty/bos/bos.py
Normal file
59
copyparty/bos/bos.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
from ..util import fsenc, fsdec
|
||||||
|
from . import path
|
||||||
|
|
||||||
|
|
||||||
|
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
||||||
|
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
||||||
|
|
||||||
|
|
||||||
|
def chmod(p, mode):
|
||||||
|
return os.chmod(fsenc(p), mode)
|
||||||
|
|
||||||
|
|
||||||
|
def listdir(p="."):
|
||||||
|
return [fsdec(x) for x in os.listdir(fsenc(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def lstat(p):
|
||||||
|
return os.lstat(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def makedirs(name, mode=0o755, exist_ok=True):
|
||||||
|
bname = fsenc(name)
|
||||||
|
try:
|
||||||
|
os.makedirs(bname, mode=mode)
|
||||||
|
except:
|
||||||
|
if not exist_ok or not os.path.isdir(bname):
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir(p, mode=0o755):
|
||||||
|
return os.mkdir(fsenc(p), mode=mode)
|
||||||
|
|
||||||
|
|
||||||
|
def rename(src, dst):
|
||||||
|
return os.rename(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
|
|
||||||
|
def replace(src, dst):
|
||||||
|
return os.replace(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
|
|
||||||
|
def rmdir(p):
|
||||||
|
return os.rmdir(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def stat(p):
|
||||||
|
return os.stat(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def unlink(p):
|
||||||
|
return os.unlink(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def utime(p, times=None):
|
||||||
|
return os.utime(fsenc(p), times)
|
||||||
33
copyparty/bos/path.py
Normal file
33
copyparty/bos/path.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
from ..util import fsenc, fsdec
|
||||||
|
|
||||||
|
|
||||||
|
def abspath(p):
|
||||||
|
return fsdec(os.path.abspath(fsenc(p)))
|
||||||
|
|
||||||
|
|
||||||
|
def exists(p):
|
||||||
|
return os.path.exists(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def getmtime(p):
|
||||||
|
return os.path.getmtime(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def getsize(p):
|
||||||
|
return os.path.getsize(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def isdir(p):
|
||||||
|
return os.path.isdir(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def islink(p):
|
||||||
|
return os.path.islink(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def realpath(p):
|
||||||
|
return fsdec(os.path.realpath(fsenc(p)))
|
||||||
@@ -22,12 +22,9 @@ class BrokerMp(object):
|
|||||||
self.retpend_mutex = threading.Lock()
|
self.retpend_mutex = threading.Lock()
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
|
||||||
cores = self.args.j
|
self.num_workers = self.args.j or mp.cpu_count()
|
||||||
if not cores:
|
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
||||||
cores = mp.cpu_count()
|
for n in range(1, self.num_workers + 1):
|
||||||
|
|
||||||
self.log("broker", "booting {} subprocesses".format(cores))
|
|
||||||
for n in range(1, cores + 1):
|
|
||||||
q_pend = mp.Queue(1)
|
q_pend = mp.Queue(1)
|
||||||
q_yield = mp.Queue(64)
|
q_yield = mp.Queue(64)
|
||||||
|
|
||||||
@@ -103,5 +100,8 @@ class BrokerMp(object):
|
|||||||
for p in self.procs:
|
for p in self.procs:
|
||||||
p.q_pend.put([0, dest, [args[0], len(self.procs)]])
|
p.q_pend.put([0, dest, [args[0], len(self.procs)]])
|
||||||
|
|
||||||
|
elif dest == "cb_httpsrv_up":
|
||||||
|
self.hub.cb_httpsrv_up()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception("what is " + str(dest))
|
raise Exception("what is " + str(dest))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class BrokerThr(object):
|
|||||||
self.asrv = hub.asrv
|
self.asrv = hub.asrv
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
self.num_workers = 1
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.httpsrv = HttpSrv(self, None)
|
self.httpsrv = HttpSrv(self, None)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import calendar
|
|||||||
|
|
||||||
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
||||||
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
||||||
|
from .bos import bos
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv
|
||||||
from .szip import StreamZip
|
from .szip import StreamZip
|
||||||
from .star import StreamTar
|
from .star import StreamTar
|
||||||
@@ -58,7 +59,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
def unpwd(self, m):
|
def unpwd(self, m):
|
||||||
a, b = m.groups()
|
a, b = m.groups()
|
||||||
return "=\033[7m {} \033[27m{}".format(self.asrv.iuser[a], b)
|
return "=\033[7m {} \033[27m{}".format(self.asrv.iacct[a], b)
|
||||||
|
|
||||||
def _check_nonfatal(self, ex):
|
def _check_nonfatal(self, ex):
|
||||||
return ex.code < 400 or ex.code in [404, 429]
|
return ex.code < 400 or ex.code in [404, 429]
|
||||||
@@ -181,9 +182,11 @@ class HttpCli(object):
|
|||||||
self.vpath = unquotep(vpath)
|
self.vpath = unquotep(vpath)
|
||||||
|
|
||||||
pwd = uparam.get("pw")
|
pwd = uparam.get("pw")
|
||||||
self.uname = self.asrv.iuser.get(pwd, "*")
|
self.uname = self.asrv.iacct.get(pwd, "*")
|
||||||
self.rvol, self.wvol, self.avol = [[], [], []]
|
self.rvol = self.asrv.vfs.aread[self.uname]
|
||||||
self.asrv.vfs.user_tree(self.uname, self.rvol, self.wvol, self.avol)
|
self.wvol = self.asrv.vfs.awrite[self.uname]
|
||||||
|
self.mvol = self.asrv.vfs.amove[self.uname]
|
||||||
|
self.dvol = self.asrv.vfs.adel[self.uname]
|
||||||
|
|
||||||
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"):
|
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"):
|
||||||
self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0]
|
self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0]
|
||||||
@@ -359,14 +362,21 @@ class HttpCli(object):
|
|||||||
self.redirect(vpath, flavor="redirecting to", use302=True)
|
self.redirect(vpath, flavor="redirecting to", use302=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.readable, self.writable = self.asrv.vfs.can_access(self.vpath, self.uname)
|
x = self.asrv.vfs.can_access(self.vpath, self.uname)
|
||||||
if not self.readable and not self.writable:
|
self.can_read, self.can_write, self.can_move, self.can_delete = x
|
||||||
|
if not self.can_read and not self.can_write:
|
||||||
if self.vpath:
|
if self.vpath:
|
||||||
self.log("inaccessible: [{}]".format(self.vpath))
|
self.log("inaccessible: [{}]".format(self.vpath))
|
||||||
raise Pebkac(404)
|
raise Pebkac(404)
|
||||||
|
|
||||||
self.uparam = {"h": False}
|
self.uparam = {"h": False}
|
||||||
|
|
||||||
|
if "delete" in self.uparam:
|
||||||
|
return self.handle_rm()
|
||||||
|
|
||||||
|
if "move" in self.uparam:
|
||||||
|
return self.handle_mv()
|
||||||
|
|
||||||
if "h" in self.uparam:
|
if "h" in self.uparam:
|
||||||
self.vpath = None
|
self.vpath = None
|
||||||
return self.tx_mounts()
|
return self.tx_mounts()
|
||||||
@@ -606,11 +616,11 @@ class HttpCli(object):
|
|||||||
if sub:
|
if sub:
|
||||||
try:
|
try:
|
||||||
dst = os.path.join(vfs.realpath, rem)
|
dst = os.path.join(vfs.realpath, rem)
|
||||||
if not os.path.isdir(fsenc(dst)):
|
if not bos.path.isdir(dst):
|
||||||
os.makedirs(fsenc(dst))
|
bos.makedirs(dst)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
self.log("makedirs failed [{}]".format(dst))
|
self.log("makedirs failed [{}]".format(dst))
|
||||||
if not os.path.isdir(fsenc(dst)):
|
if not bos.path.isdir(dst):
|
||||||
if ex.errno == 13:
|
if ex.errno == 13:
|
||||||
raise Pebkac(500, "the server OS denied write-access")
|
raise Pebkac(500, "the server OS denied write-access")
|
||||||
|
|
||||||
@@ -756,7 +766,7 @@ class HttpCli(object):
|
|||||||
times = (int(time.time()), int(lastmod))
|
times = (int(time.time()), int(lastmod))
|
||||||
self.log("no more chunks, setting times {}".format(times))
|
self.log("no more chunks, setting times {}".format(times))
|
||||||
try:
|
try:
|
||||||
os.utime(fsenc(path), times)
|
bos.utime(path, times)
|
||||||
except:
|
except:
|
||||||
self.log("failed to utime ({}, {})".format(path, times))
|
self.log("failed to utime ({}, {})".format(path, times))
|
||||||
|
|
||||||
@@ -775,7 +785,7 @@ class HttpCli(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_pwd_cookie(self, pwd):
|
def get_pwd_cookie(self, pwd):
|
||||||
if pwd in self.asrv.iuser:
|
if pwd in self.asrv.iacct:
|
||||||
msg = "login ok"
|
msg = "login ok"
|
||||||
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
|
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
|
||||||
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||||
@@ -801,14 +811,14 @@ class HttpCli(object):
|
|||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
fn = os.path.join(fdir, sanitized)
|
fn = os.path.join(fdir, sanitized)
|
||||||
|
|
||||||
if not os.path.isdir(fsenc(fdir)):
|
if not bos.path.isdir(fdir):
|
||||||
raise Pebkac(500, "parent folder does not exist")
|
raise Pebkac(500, "parent folder does not exist")
|
||||||
|
|
||||||
if os.path.isdir(fsenc(fn)):
|
if bos.path.isdir(fn):
|
||||||
raise Pebkac(500, "that folder exists already")
|
raise Pebkac(500, "that folder exists already")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.mkdir(fsenc(fn))
|
bos.mkdir(fn)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
if ex.errno == 13:
|
if ex.errno == 13:
|
||||||
raise Pebkac(500, "the server OS denied write-access")
|
raise Pebkac(500, "the server OS denied write-access")
|
||||||
@@ -838,7 +848,7 @@ class HttpCli(object):
|
|||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
fn = os.path.join(fdir, sanitized)
|
fn = os.path.join(fdir, sanitized)
|
||||||
|
|
||||||
if os.path.exists(fsenc(fn)):
|
if bos.path.exists(fn):
|
||||||
raise Pebkac(500, "that file exists already")
|
raise Pebkac(500, "that file exists already")
|
||||||
|
|
||||||
with open(fsenc(fn), "wb") as f:
|
with open(fsenc(fn), "wb") as f:
|
||||||
@@ -868,7 +878,7 @@ class HttpCli(object):
|
|||||||
p_file, "", [".prologue.html", ".epilogue.html"]
|
p_file, "", [".prologue.html", ".epilogue.html"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.isdir(fsenc(fdir)):
|
if not bos.path.isdir(fdir):
|
||||||
raise Pebkac(404, "that folder does not exist")
|
raise Pebkac(404, "that folder does not exist")
|
||||||
|
|
||||||
suffix = ".{:.6f}-{}".format(time.time(), self.ip)
|
suffix = ".{:.6f}-{}".format(time.time(), self.ip)
|
||||||
@@ -907,10 +917,10 @@ class HttpCli(object):
|
|||||||
|
|
||||||
suffix = ".PARTIAL"
|
suffix = ".PARTIAL"
|
||||||
try:
|
try:
|
||||||
os.rename(fsenc(fp), fsenc(fp2 + suffix))
|
bos.rename(fp, fp2 + suffix)
|
||||||
except:
|
except:
|
||||||
fp2 = fp2[: -len(suffix) - 1]
|
fp2 = fp2[: -len(suffix) - 1]
|
||||||
os.rename(fsenc(fp), fsenc(fp2 + suffix))
|
bos.rename(fp, fp2 + suffix)
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -994,13 +1004,6 @@ class HttpCli(object):
|
|||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
# TODO:
|
|
||||||
# the per-volume read/write permissions must be replaced with permission flags
|
|
||||||
# which would decide how to handle uploads to filenames which are taken,
|
|
||||||
# current behavior of creating a new name is a good default for binary files
|
|
||||||
# but should also offer a flag to takeover the filename and rename the old one
|
|
||||||
#
|
|
||||||
# stopgap:
|
|
||||||
if not rem.endswith(".md"):
|
if not rem.endswith(".md"):
|
||||||
raise Pebkac(400, "only markdown pls")
|
raise Pebkac(400, "only markdown pls")
|
||||||
|
|
||||||
@@ -1015,7 +1018,7 @@ class HttpCli(object):
|
|||||||
fp = os.path.join(vfs.realpath, rem)
|
fp = os.path.join(vfs.realpath, rem)
|
||||||
srv_lastmod = srv_lastmod3 = -1
|
srv_lastmod = srv_lastmod3 = -1
|
||||||
try:
|
try:
|
||||||
st = os.stat(fsenc(fp))
|
st = bos.stat(fp)
|
||||||
srv_lastmod = st.st_mtime
|
srv_lastmod = st.st_mtime
|
||||||
srv_lastmod3 = int(srv_lastmod * 1000)
|
srv_lastmod3 = int(srv_lastmod * 1000)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
@@ -1051,14 +1054,13 @@ class HttpCli(object):
|
|||||||
self.reply(response.encode("utf-8"))
|
self.reply(response.encode("utf-8"))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# TODO another hack re: pending permissions rework
|
|
||||||
mdir, mfile = os.path.split(fp)
|
mdir, mfile = os.path.split(fp)
|
||||||
mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod)
|
mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod)
|
||||||
try:
|
try:
|
||||||
os.mkdir(fsenc(os.path.join(mdir, ".hist")))
|
bos.mkdir(os.path.join(mdir, ".hist"))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
os.rename(fsenc(fp), fsenc(os.path.join(mdir, ".hist", mfile2)))
|
bos.rename(fp, os.path.join(mdir, ".hist", mfile2))
|
||||||
|
|
||||||
p_field, _, p_data = next(self.parser.gen)
|
p_field, _, p_data = next(self.parser.gen)
|
||||||
if p_field != "body":
|
if p_field != "body":
|
||||||
@@ -1067,7 +1069,7 @@ class HttpCli(object):
|
|||||||
with open(fsenc(fp), "wb", 512 * 1024) as f:
|
with open(fsenc(fp), "wb", 512 * 1024) as f:
|
||||||
sz, sha512, _ = hashcopy(p_data, f)
|
sz, sha512, _ = hashcopy(p_data, f)
|
||||||
|
|
||||||
new_lastmod = os.stat(fsenc(fp)).st_mtime
|
new_lastmod = bos.stat(fp).st_mtime
|
||||||
new_lastmod3 = int(new_lastmod * 1000)
|
new_lastmod3 = int(new_lastmod * 1000)
|
||||||
sha512 = sha512[:56]
|
sha512 = sha512[:56]
|
||||||
|
|
||||||
@@ -1112,7 +1114,7 @@ class HttpCli(object):
|
|||||||
for ext in ["", ".gz", ".br"]:
|
for ext in ["", ".gz", ".br"]:
|
||||||
try:
|
try:
|
||||||
fs_path = req_path + ext
|
fs_path = req_path + ext
|
||||||
st = os.stat(fsenc(fs_path))
|
st = bos.stat(fs_path)
|
||||||
file_ts = max(file_ts, st.st_mtime)
|
file_ts = max(file_ts, st.st_mtime)
|
||||||
editions[ext or "plain"] = [fs_path, st.st_size]
|
editions[ext or "plain"] = [fs_path, st.st_size]
|
||||||
except:
|
except:
|
||||||
@@ -1364,10 +1366,10 @@ class HttpCli(object):
|
|||||||
html_path = os.path.join(E.mod, "web", "{}.html".format(tpl))
|
html_path = os.path.join(E.mod, "web", "{}.html".format(tpl))
|
||||||
template = self.j2(tpl)
|
template = self.j2(tpl)
|
||||||
|
|
||||||
st = os.stat(fsenc(fs_path))
|
st = bos.stat(fs_path)
|
||||||
ts_md = st.st_mtime
|
ts_md = st.st_mtime
|
||||||
|
|
||||||
st = os.stat(fsenc(html_path))
|
st = bos.stat(html_path)
|
||||||
ts_html = st.st_mtime
|
ts_html = st.st_mtime
|
||||||
|
|
||||||
sz_md = 0
|
sz_md = 0
|
||||||
@@ -1424,12 +1426,13 @@ class HttpCli(object):
|
|||||||
|
|
||||||
def tx_mounts(self):
|
def tx_mounts(self):
|
||||||
suf = self.urlq({}, ["h"])
|
suf = self.urlq({}, ["h"])
|
||||||
|
avol = [x for x in self.wvol if x in self.rvol]
|
||||||
rvol, wvol, avol = [
|
rvol, wvol, avol = [
|
||||||
[("/" + x).rstrip("/") + "/" for x in y]
|
[("/" + x).rstrip("/") + "/" for x in y]
|
||||||
for y in [self.rvol, self.wvol, self.avol]
|
for y in [self.rvol, self.wvol, avol]
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.avol and not self.args.no_rescan:
|
if avol and not self.args.no_rescan:
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.get_state")
|
x = self.conn.hsrv.broker.put(True, "up2k.get_state")
|
||||||
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()}
|
||||||
@@ -1454,8 +1457,8 @@ class HttpCli(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def scanvol(self):
|
def scanvol(self):
|
||||||
if not self.readable or not self.writable:
|
if not self.can_read or not self.can_write:
|
||||||
raise Pebkac(403, "not admin")
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
if self.args.no_rescan:
|
if self.args.no_rescan:
|
||||||
raise Pebkac(403, "disabled by argv")
|
raise Pebkac(403, "disabled by argv")
|
||||||
@@ -1473,8 +1476,8 @@ class HttpCli(object):
|
|||||||
raise Pebkac(500, x)
|
raise Pebkac(500, x)
|
||||||
|
|
||||||
def tx_stack(self):
|
def tx_stack(self):
|
||||||
if not self.avol:
|
if not [x for x in self.wvol if x in self.rvol]:
|
||||||
raise Pebkac(403, "not admin")
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
if self.args.no_stack:
|
if self.args.no_stack:
|
||||||
raise Pebkac(403, "disabled by argv")
|
raise Pebkac(403, "disabled by argv")
|
||||||
@@ -1512,7 +1515,7 @@ class HttpCli(object):
|
|||||||
try:
|
try:
|
||||||
vn, rem = self.asrv.vfs.get(top, self.uname, True, False)
|
vn, rem = self.asrv.vfs.get(top, self.uname, True, False)
|
||||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||||
rem, self.uname, not self.args.no_scandir, incl_wo=True
|
rem, self.uname, not self.args.no_scandir, [[True], [False, True]]
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
vfs_ls = []
|
vfs_ls = []
|
||||||
@@ -1539,6 +1542,33 @@ class HttpCli(object):
|
|||||||
ret["a"] = dirs
|
ret["a"] = dirs
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def handle_rm(self):
|
||||||
|
if not self.can_delete:
|
||||||
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
|
if self.args.no_del:
|
||||||
|
raise Pebkac(403, "disabled by argv")
|
||||||
|
|
||||||
|
x = self.conn.hsrv.broker.put(True, "up2k.handle_rm", self.uname, self.vpath)
|
||||||
|
self.loud_reply(x.get())
|
||||||
|
|
||||||
|
def handle_mv(self):
|
||||||
|
if not self.can_move:
|
||||||
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
|
if self.args.no_mv:
|
||||||
|
raise Pebkac(403, "disabled by argv")
|
||||||
|
|
||||||
|
# full path of new loc (incl filename)
|
||||||
|
dst = self.uparam.get("move")
|
||||||
|
if not dst:
|
||||||
|
raise Pebkac(400, "need dst vpath")
|
||||||
|
|
||||||
|
x = self.conn.hsrv.broker.put(
|
||||||
|
True, "up2k.handle_mv", self.uname, self.vpath, dst
|
||||||
|
)
|
||||||
|
self.loud_reply(x.get())
|
||||||
|
|
||||||
def tx_browser(self):
|
def tx_browser(self):
|
||||||
vpath = ""
|
vpath = ""
|
||||||
vpnodes = [["", "/"]]
|
vpnodes = [["", "/"]]
|
||||||
@@ -1551,18 +1581,16 @@ class HttpCli(object):
|
|||||||
|
|
||||||
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
||||||
|
|
||||||
vn, rem = self.asrv.vfs.get(
|
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
||||||
self.vpath, self.uname, self.readable, self.writable
|
|
||||||
)
|
|
||||||
abspath = vn.canonical(rem)
|
abspath = vn.canonical(rem)
|
||||||
dbv, vrem = vn.get_dbv(rem)
|
dbv, vrem = vn.get_dbv(rem)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = os.stat(fsenc(abspath))
|
st = bos.stat(abspath)
|
||||||
except:
|
except:
|
||||||
raise Pebkac(404)
|
raise Pebkac(404)
|
||||||
|
|
||||||
if self.readable:
|
if self.can_read:
|
||||||
if rem.startswith(".hist/up2k.") or (
|
if rem.startswith(".hist/up2k.") or (
|
||||||
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
||||||
):
|
):
|
||||||
@@ -1574,7 +1602,7 @@ class HttpCli(object):
|
|||||||
if is_dir:
|
if is_dir:
|
||||||
for fn in self.args.th_covers.split(","):
|
for fn in self.args.th_covers.split(","):
|
||||||
fp = os.path.join(abspath, fn)
|
fp = os.path.join(abspath, fn)
|
||||||
if os.path.exists(fp):
|
if bos.path.exists(fp):
|
||||||
vrem = "{}/{}".format(vrem.rstrip("/"), fn)
|
vrem = "{}/{}".format(vrem.rstrip("/"), fn)
|
||||||
is_dir = False
|
is_dir = False
|
||||||
break
|
break
|
||||||
@@ -1629,10 +1657,14 @@ class HttpCli(object):
|
|||||||
srv_info = "</span> /// <span>".join(srv_info)
|
srv_info = "</span> /// <span>".join(srv_info)
|
||||||
|
|
||||||
perms = []
|
perms = []
|
||||||
if self.readable:
|
if self.can_read:
|
||||||
perms.append("read")
|
perms.append("read")
|
||||||
if self.writable:
|
if self.can_write:
|
||||||
perms.append("write")
|
perms.append("write")
|
||||||
|
if self.can_move:
|
||||||
|
perms.append("move")
|
||||||
|
if self.can_delete:
|
||||||
|
perms.append("delete")
|
||||||
|
|
||||||
url_suf = self.urlq({}, [])
|
url_suf = self.urlq({}, [])
|
||||||
is_ls = "ls" in self.uparam
|
is_ls = "ls" in self.uparam
|
||||||
@@ -1644,7 +1676,7 @@ class HttpCli(object):
|
|||||||
logues = ["", ""]
|
logues = ["", ""]
|
||||||
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
|
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
|
||||||
fn = os.path.join(abspath, fn)
|
fn = os.path.join(abspath, fn)
|
||||||
if os.path.exists(fsenc(fn)):
|
if bos.path.exists(fn):
|
||||||
with open(fsenc(fn), "rb") as f:
|
with open(fsenc(fn), "rb") as f:
|
||||||
logues[n] = f.read().decode("utf-8")
|
logues[n] = f.read().decode("utf-8")
|
||||||
|
|
||||||
@@ -1653,6 +1685,7 @@ class HttpCli(object):
|
|||||||
"files": [],
|
"files": [],
|
||||||
"taglist": [],
|
"taglist": [],
|
||||||
"srvinf": srv_info,
|
"srvinf": srv_info,
|
||||||
|
"acct": self.uname,
|
||||||
"perms": perms,
|
"perms": perms,
|
||||||
"logues": logues,
|
"logues": logues,
|
||||||
}
|
}
|
||||||
@@ -1660,19 +1693,22 @@ class HttpCli(object):
|
|||||||
"vdir": quotep(self.vpath),
|
"vdir": quotep(self.vpath),
|
||||||
"vpnodes": vpnodes,
|
"vpnodes": vpnodes,
|
||||||
"files": [],
|
"files": [],
|
||||||
|
"acct": self.uname,
|
||||||
"perms": json.dumps(perms),
|
"perms": json.dumps(perms),
|
||||||
"taglist": [],
|
"taglist": [],
|
||||||
"tag_order": [],
|
"tag_order": [],
|
||||||
"have_up2k_idx": ("e2d" in vn.flags),
|
"have_up2k_idx": ("e2d" in vn.flags),
|
||||||
"have_tags_idx": ("e2t" in vn.flags),
|
"have_tags_idx": ("e2t" in vn.flags),
|
||||||
|
"have_mv": (not self.args.no_mv),
|
||||||
|
"have_del": (not self.args.no_del),
|
||||||
"have_zip": (not self.args.no_zip),
|
"have_zip": (not self.args.no_zip),
|
||||||
"have_b_u": (self.writable and self.uparam.get("b") == "u"),
|
"have_b_u": (self.can_write and self.uparam.get("b") == "u"),
|
||||||
"url_suf": url_suf,
|
"url_suf": url_suf,
|
||||||
"logues": logues,
|
"logues": logues,
|
||||||
"title": html_escape(self.vpath, crlf=True),
|
"title": html_escape(self.vpath, crlf=True),
|
||||||
"srv_info": srv_info,
|
"srv_info": srv_info,
|
||||||
}
|
}
|
||||||
if not self.readable:
|
if not self.can_read:
|
||||||
if is_ls:
|
if is_ls:
|
||||||
ret = json.dumps(ls_ret)
|
ret = json.dumps(ls_ret)
|
||||||
self.reply(
|
self.reply(
|
||||||
@@ -1695,7 +1731,7 @@ class HttpCli(object):
|
|||||||
return self.tx_zip(k, v, vn, rem, [], self.args.ed)
|
return self.tx_zip(k, v, vn, rem, [], self.args.ed)
|
||||||
|
|
||||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||||
rem, self.uname, not self.args.no_scandir, incl_wo=True
|
rem, self.uname, not self.args.no_scandir, [[True], [False, True]]
|
||||||
)
|
)
|
||||||
stats = {k: v for k, v in vfs_ls}
|
stats = {k: v for k, v in vfs_ls}
|
||||||
vfs_ls = [x[0] for x in vfs_ls]
|
vfs_ls = [x[0] for x in vfs_ls]
|
||||||
@@ -1706,7 +1742,7 @@ class HttpCli(object):
|
|||||||
histdir = os.path.join(fsroot, ".hist")
|
histdir = os.path.join(fsroot, ".hist")
|
||||||
ptn = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[^\.]+)$")
|
ptn = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[^\.]+)$")
|
||||||
try:
|
try:
|
||||||
for hfn in os.listdir(histdir):
|
for hfn in bos.listdir(histdir):
|
||||||
m = ptn.match(hfn)
|
m = ptn.match(hfn)
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
@@ -1747,7 +1783,7 @@ class HttpCli(object):
|
|||||||
fspath = fsroot + "/" + fn
|
fspath = fsroot + "/" + fn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
inf = stats.get(fn) or os.stat(fsenc(fspath))
|
inf = stats.get(fn) or bos.stat(fspath)
|
||||||
except:
|
except:
|
||||||
self.log("broken symlink: {}".format(repr(fspath)))
|
self.log("broken symlink: {}".format(repr(fspath)))
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ except ImportError:
|
|||||||
|
|
||||||
from .__init__ import E, PY2, MACOS
|
from .__init__ import E, PY2, MACOS
|
||||||
from .util import spack, min_ex, start_stackmon, start_log_thrs
|
from .util import spack, min_ex, start_stackmon, start_log_thrs
|
||||||
|
from .bos import bos
|
||||||
from .httpconn import HttpConn
|
from .httpconn import HttpConn
|
||||||
|
|
||||||
if PY2:
|
if PY2:
|
||||||
@@ -73,7 +74,7 @@ class HttpSrv(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cert_path = os.path.join(E.cfg, "cert.pem")
|
cert_path = os.path.join(E.cfg, "cert.pem")
|
||||||
if os.path.exists(cert_path):
|
if bos.path.exists(cert_path):
|
||||||
self.cert_path = cert_path
|
self.cert_path = cert_path
|
||||||
else:
|
else:
|
||||||
self.cert_path = None
|
self.cert_path = None
|
||||||
@@ -140,6 +141,7 @@ class HttpSrv(object):
|
|||||||
fno = srv_sck.fileno()
|
fno = srv_sck.fileno()
|
||||||
msg = "subscribed @ {}:{} f{}".format(ip, port, fno)
|
msg = "subscribed @ {}:{} f{}".format(ip, port, fno)
|
||||||
self.log(self.name, msg)
|
self.log(self.name, msg)
|
||||||
|
self.broker.put(False, "cb_httpsrv_up")
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="1;30")
|
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="1;30")
|
||||||
@@ -307,7 +309,7 @@ class HttpSrv(object):
|
|||||||
try:
|
try:
|
||||||
with os.scandir(os.path.join(E.mod, "web")) as dh:
|
with os.scandir(os.path.join(E.mod, "web")) as dh:
|
||||||
for fh in dh:
|
for fh in dh:
|
||||||
inf = fh.stat(follow_symlinks=False)
|
inf = fh.stat()
|
||||||
v = max(v, inf.st_mtime)
|
v = max(v, inf.st_mtime)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import subprocess as sp
|
|||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, unicode
|
from .__init__ import PY2, WINDOWS, unicode
|
||||||
from .util import fsenc, fsdec, uncyg, REKOBO_LKEY
|
from .util import fsenc, fsdec, uncyg, REKOBO_LKEY
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def have_ff(cmd):
|
def have_ff(cmd):
|
||||||
@@ -44,7 +45,7 @@ class MParser(object):
|
|||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
bp = uncyg(bp)
|
bp = uncyg(bp)
|
||||||
|
|
||||||
if os.path.exists(bp):
|
if bos.path.exists(bp):
|
||||||
self.bin = bp
|
self.bin = bp
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
@@ -227,37 +228,47 @@ def parse_ffprobe(txt):
|
|||||||
class MTag(object):
|
class MTag(object):
|
||||||
def __init__(self, log_func, args):
|
def __init__(self, log_func, args):
|
||||||
self.log_func = log_func
|
self.log_func = log_func
|
||||||
|
self.args = args
|
||||||
self.usable = True
|
self.usable = True
|
||||||
self.prefer_mt = False
|
self.prefer_mt = not args.no_mtag_ff
|
||||||
mappings = args.mtm
|
|
||||||
self.backend = "ffprobe" if args.no_mutagen else "mutagen"
|
self.backend = "ffprobe" if args.no_mutagen else "mutagen"
|
||||||
or_ffprobe = " or ffprobe"
|
self.can_ffprobe = (
|
||||||
|
HAVE_FFPROBE
|
||||||
|
and not args.no_mtag_ff
|
||||||
|
and (not WINDOWS or sys.version_info >= (3, 8))
|
||||||
|
)
|
||||||
|
mappings = args.mtm
|
||||||
|
or_ffprobe = " or FFprobe"
|
||||||
|
|
||||||
if self.backend == "mutagen":
|
if self.backend == "mutagen":
|
||||||
self.get = self.get_mutagen
|
self.get = self.get_mutagen
|
||||||
try:
|
try:
|
||||||
import mutagen
|
import mutagen
|
||||||
except:
|
except:
|
||||||
self.log("could not load mutagen, trying ffprobe instead", c=3)
|
self.log("could not load Mutagen, trying FFprobe instead", c=3)
|
||||||
self.backend = "ffprobe"
|
self.backend = "ffprobe"
|
||||||
|
|
||||||
if self.backend == "ffprobe":
|
if self.backend == "ffprobe":
|
||||||
|
self.usable = self.can_ffprobe
|
||||||
self.get = self.get_ffprobe
|
self.get = self.get_ffprobe
|
||||||
self.prefer_mt = True
|
self.prefer_mt = True
|
||||||
# about 20x slower
|
|
||||||
self.usable = HAVE_FFPROBE
|
|
||||||
|
|
||||||
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
if not HAVE_FFPROBE:
|
||||||
self.usable = False
|
pass
|
||||||
|
|
||||||
|
elif args.no_mtag_ff:
|
||||||
|
msg = "found FFprobe but it was disabled by --no-mtag-ff"
|
||||||
|
self.log(msg, c=3)
|
||||||
|
|
||||||
|
elif WINDOWS and sys.version_info < (3, 8):
|
||||||
or_ffprobe = " or python >= 3.8"
|
or_ffprobe = " or python >= 3.8"
|
||||||
msg = "found ffprobe but your python is too old; need 3.8 or newer"
|
msg = "found FFprobe but your python is too old; need 3.8 or newer"
|
||||||
self.log(msg, c=1)
|
self.log(msg, c=1)
|
||||||
|
|
||||||
if not self.usable:
|
if not self.usable:
|
||||||
msg = "need mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
msg = "need Mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
||||||
self.log(
|
pybin = os.path.basename(sys.executable)
|
||||||
msg.format(or_ffprobe, " " * 37, os.path.basename(sys.executable)), c=1
|
self.log(msg.format(or_ffprobe, " " * 37, pybin), c=1)
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||||
@@ -387,7 +398,7 @@ class MTag(object):
|
|||||||
v2 = r2.get(k)
|
v2 = r2.get(k)
|
||||||
if v1 == v2:
|
if v1 == v2:
|
||||||
print(" ", k, v1)
|
print(" ", k, v1)
|
||||||
elif v1 != "0000": # ffprobe date=0
|
elif v1 != "0000": # FFprobe date=0
|
||||||
diffs.append(k)
|
diffs.append(k)
|
||||||
print(" 1", k, v1)
|
print(" 1", k, v1)
|
||||||
print(" 2", k, v2)
|
print(" 2", k, v2)
|
||||||
@@ -408,20 +419,33 @@ class MTag(object):
|
|||||||
md = mutagen.File(fsenc(abspath), easy=True)
|
md = mutagen.File(fsenc(abspath), easy=True)
|
||||||
x = md.info.length
|
x = md.info.length
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
return {}
|
return self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
||||||
|
|
||||||
ret = {}
|
sz = bos.path.getsize(abspath)
|
||||||
try:
|
ret = {".q": [0, int((sz / md.info.length) / 128)]}
|
||||||
dur = int(md.info.length)
|
|
||||||
|
for attr, k, norm in [
|
||||||
|
["codec", "ac", unicode],
|
||||||
|
["channels", "chs", int],
|
||||||
|
["sample_rate", ".hz", int],
|
||||||
|
["bitrate", ".aq", int],
|
||||||
|
["length", ".dur", int],
|
||||||
|
]:
|
||||||
try:
|
try:
|
||||||
q = int(md.info.bitrate / 1024)
|
v = getattr(md.info, attr)
|
||||||
except:
|
except:
|
||||||
q = int((os.path.getsize(fsenc(abspath)) / dur) / 128)
|
continue
|
||||||
|
|
||||||
ret[".dur"] = [0, dur]
|
if not v:
|
||||||
ret[".q"] = [0, q]
|
continue
|
||||||
except:
|
|
||||||
pass
|
if k == ".aq":
|
||||||
|
v /= 1000
|
||||||
|
|
||||||
|
if k == "ac" and v.startswith("mp4a.40."):
|
||||||
|
v = "aac"
|
||||||
|
|
||||||
|
ret[k] = [0, norm(v)]
|
||||||
|
|
||||||
return self.normalize_tags(ret, md)
|
return self.normalize_tags(ret, md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import tarfile
|
import tarfile
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .sutil import errdesc
|
from .sutil import errdesc
|
||||||
from .util import Queue, fsenc
|
from .util import Queue, fsenc
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
class QFile(object):
|
class QFile(object):
|
||||||
@@ -61,7 +61,7 @@ class StreamTar(object):
|
|||||||
|
|
||||||
yield None
|
yield None
|
||||||
if self.errf:
|
if self.errf:
|
||||||
os.unlink(self.errf["ap"])
|
bos.unlink(self.errf["ap"])
|
||||||
|
|
||||||
def ser(self, f):
|
def ser(self, f):
|
||||||
name = f["vp"]
|
name = f["vp"]
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def errdesc(errors):
|
def errdesc(errors):
|
||||||
report = ["copyparty failed to add the following files to the archive:", ""]
|
report = ["copyparty failed to add the following files to the archive:", ""]
|
||||||
@@ -20,9 +21,9 @@ def errdesc(errors):
|
|||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcfromtimestamp(time.time())
|
||||||
dt = dt.strftime("%Y-%m%d-%H%M%S")
|
dt = dt.strftime("%Y-%m%d-%H%M%S")
|
||||||
|
|
||||||
os.chmod(tf_path, 0o444)
|
bos.chmod(tf_path, 0o444)
|
||||||
return {
|
return {
|
||||||
"vp": "archive-errors-{}.txt".format(dt),
|
"vp": "archive-errors-{}.txt".format(dt),
|
||||||
"ap": tf_path,
|
"ap": tf_path,
|
||||||
"st": os.stat(tf_path),
|
"st": bos.stat(tf_path),
|
||||||
}, report
|
}, report
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import threading
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from .__init__ import E, PY2, WINDOWS, MACOS, VT100, unicode
|
from .__init__ import E, PY2, WINDOWS, ANYWIN, MACOS, VT100, unicode
|
||||||
from .util import mp, start_log_thrs, start_stackmon, min_ex
|
from .util import mp, start_log_thrs, start_stackmon, min_ex
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
@@ -39,6 +39,7 @@ class SvcHub(object):
|
|||||||
self.stop_req = False
|
self.stop_req = False
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.stop_cond = threading.Condition()
|
self.stop_cond = threading.Condition()
|
||||||
|
self.httpsrv_up = 0
|
||||||
|
|
||||||
self.ansi_re = re.compile("\033\\[[^m]*m")
|
self.ansi_re = re.compile("\033\\[[^m]*m")
|
||||||
self.log_mutex = threading.Lock()
|
self.log_mutex = threading.Lock()
|
||||||
@@ -55,7 +56,7 @@ class SvcHub(object):
|
|||||||
start_log_thrs(self.log, args.log_thrs, 0)
|
start_log_thrs(self.log, args.log_thrs, 0)
|
||||||
|
|
||||||
# initiate all services to manage
|
# initiate all services to manage
|
||||||
self.asrv = AuthSrv(self.args, self.log, False)
|
self.asrv = AuthSrv(self.args, self.log)
|
||||||
if args.ls:
|
if args.ls:
|
||||||
self.asrv.dbg_ls()
|
self.asrv.dbg_ls()
|
||||||
|
|
||||||
@@ -86,6 +87,29 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.broker = Broker(self)
|
self.broker = Broker(self)
|
||||||
|
|
||||||
|
def thr_httpsrv_up(self):
|
||||||
|
time.sleep(5)
|
||||||
|
failed = self.broker.num_workers - self.httpsrv_up
|
||||||
|
if not failed:
|
||||||
|
return
|
||||||
|
|
||||||
|
m = "{}/{} workers failed to start"
|
||||||
|
m = m.format(failed, self.broker.num_workers)
|
||||||
|
self.log("root", m, 1)
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
def cb_httpsrv_up(self):
|
||||||
|
self.httpsrv_up += 1
|
||||||
|
if self.httpsrv_up != self.broker.num_workers:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("root", "workers OK\n")
|
||||||
|
self.up2k.init_vols()
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self.sd_notify, name="sd-notify")
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
def _logname(self):
|
def _logname(self):
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcfromtimestamp(time.time())
|
||||||
fn = self.args.lo
|
fn = self.args.lo
|
||||||
@@ -135,24 +159,33 @@ class SvcHub(object):
|
|||||||
def run(self):
|
def run(self):
|
||||||
self.tcpsrv.run()
|
self.tcpsrv.run()
|
||||||
|
|
||||||
thr = threading.Thread(target=self.sd_notify, name="sd-notify")
|
thr = threading.Thread(target=self.thr_httpsrv_up)
|
||||||
thr.daemon = True
|
|
||||||
thr.start()
|
|
||||||
|
|
||||||
thr = threading.Thread(target=self.stop_thr, name="svchub-sig")
|
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
for sig in [signal.SIGINT, signal.SIGTERM]:
|
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||||
signal.signal(sig, self.signal_handler)
|
signal.signal(sig, self.signal_handler)
|
||||||
|
|
||||||
try:
|
# macos hangs after shutdown on sigterm with while-sleep,
|
||||||
while not self.stop_req:
|
# windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??)
|
||||||
time.sleep(9001)
|
# linux is fine with both,
|
||||||
except:
|
# never lucky
|
||||||
pass
|
if ANYWIN:
|
||||||
|
# msys-python probably fine but >msys-python
|
||||||
|
thr = threading.Thread(target=self.stop_thr, name="svchub-sig")
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
self.shutdown()
|
try:
|
||||||
|
while not self.stop_req:
|
||||||
|
time.sleep(1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
thr.join()
|
||||||
|
else:
|
||||||
|
self.stop_thr()
|
||||||
|
|
||||||
def stop_thr(self):
|
def stop_thr(self):
|
||||||
while not self.stop_req:
|
while not self.stop_req:
|
||||||
@@ -161,7 +194,7 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
|
|
||||||
def signal_handler(self):
|
def signal_handler(self, sig, frame):
|
||||||
if self.stopping:
|
if self.stopping:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -175,6 +208,10 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.stopping = True
|
self.stopping = True
|
||||||
self.stop_req = True
|
self.stop_req = True
|
||||||
|
with self.stop_cond:
|
||||||
|
self.stop_cond.notify_all()
|
||||||
|
|
||||||
|
ret = 1
|
||||||
try:
|
try:
|
||||||
with self.log_mutex:
|
with self.log_mutex:
|
||||||
print("OPYTHAT")
|
print("OPYTHAT")
|
||||||
@@ -194,11 +231,14 @@ class SvcHub(object):
|
|||||||
print("waiting for thumbsrv (10sec)...")
|
print("waiting for thumbsrv (10sec)...")
|
||||||
|
|
||||||
print("nailed it", end="")
|
print("nailed it", end="")
|
||||||
|
ret = 0
|
||||||
finally:
|
finally:
|
||||||
print("\033[0m")
|
print("\033[0m")
|
||||||
if self.logf:
|
if self.logf:
|
||||||
self.logf.close()
|
self.logf.close()
|
||||||
|
|
||||||
|
sys.exit(ret)
|
||||||
|
|
||||||
def _log_disabled(self, src, msg, c=0):
|
def _log_disabled(self, src, msg, c=0):
|
||||||
if not self.logf:
|
if not self.logf:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from .sutil import errdesc
|
from .sutil import errdesc
|
||||||
from .util import yieldfile, sanitize_fn, spack, sunpack
|
from .util import yieldfile, sanitize_fn, spack, sunpack
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def dostime2unix(buf):
|
def dostime2unix(buf):
|
||||||
@@ -271,4 +272,4 @@ class StreamZip(object):
|
|||||||
yield self._ct(ecdr)
|
yield self._ct(ecdr)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
os.unlink(errf["ap"])
|
bos.unlink(errf["ap"])
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
from .__init__ import MACOS, ANYWIN
|
||||||
from .util import chkcmd
|
from .util import chkcmd
|
||||||
|
|
||||||
|
|
||||||
@@ -29,14 +30,16 @@ class TcpSrv(object):
|
|||||||
for x in nonlocals:
|
for x in nonlocals:
|
||||||
eps[x] = "external"
|
eps[x] = "external"
|
||||||
|
|
||||||
|
msgs = []
|
||||||
|
m = "available @ http://{}:{}/ (\033[33m{}\033[0m)"
|
||||||
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
||||||
for port in sorted(self.args.p):
|
for port in sorted(self.args.p):
|
||||||
self.log(
|
msgs.append(m.format(ip, port, desc))
|
||||||
"tcpsrv",
|
|
||||||
"available @ http://{}:{}/ (\033[33m{}\033[0m)".format(
|
if msgs:
|
||||||
ip, port, desc
|
msgs[-1] += "\n"
|
||||||
),
|
for m in msgs:
|
||||||
)
|
self.log("tcpsrv", m)
|
||||||
|
|
||||||
self.srv = []
|
self.srv = []
|
||||||
for ip in self.args.i:
|
for ip in self.args.i:
|
||||||
@@ -81,25 +84,100 @@ class TcpSrv(object):
|
|||||||
|
|
||||||
self.log("tcpsrv", "ok bye")
|
self.log("tcpsrv", "ok bye")
|
||||||
|
|
||||||
def detect_interfaces(self, listen_ips):
|
def ips_linux(self):
|
||||||
eps = {}
|
eps = {}
|
||||||
|
|
||||||
# get all ips and their interfaces
|
|
||||||
try:
|
try:
|
||||||
ip_addr, _ = chkcmd("ip", "addr")
|
txt, _ = chkcmd(["ip", "addr"])
|
||||||
except:
|
except:
|
||||||
ip_addr = None
|
return eps
|
||||||
|
|
||||||
if ip_addr:
|
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
|
||||||
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
|
for ln in txt.split("\n"):
|
||||||
for ln in ip_addr.split("\n"):
|
try:
|
||||||
try:
|
ip, dev = r.match(ln.rstrip()).groups()
|
||||||
ip, dev = r.match(ln.rstrip()).groups()
|
eps[ip] = dev
|
||||||
for lip in listen_ips:
|
except:
|
||||||
if lip in ["0.0.0.0", ip]:
|
pass
|
||||||
eps[ip] = dev
|
|
||||||
except:
|
return eps
|
||||||
pass
|
|
||||||
|
def ips_macos(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd(["ifconfig"])
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r"^([^ ]+):")
|
||||||
|
rip = re.compile(r"^\tinet ([0-9\.]+) ")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1)
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def ips_windows_ipconfig(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd(["ipconfig"])
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r"(^[^ ].*):$")
|
||||||
|
rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.replace("\r", "").split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1).split(" adapter ", 1)[-1]
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m and dev:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def ips_windows_netsh(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd("netsh interface ip show address".split())
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r'.* "([^"]+)"$')
|
||||||
|
rip = re.compile(r".* IP\b.*: +([0-9\.]{7,15})$")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.replace("\r", "").split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1)
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m and dev:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def detect_interfaces(self, listen_ips):
|
||||||
|
if MACOS:
|
||||||
|
eps = self.ips_macos()
|
||||||
|
elif ANYWIN:
|
||||||
|
eps = self.ips_windows_ipconfig() # sees more interfaces
|
||||||
|
eps.update(self.ips_windows_netsh()) # has better names
|
||||||
|
else:
|
||||||
|
eps = self.ips_linux()
|
||||||
|
|
||||||
|
if "0.0.0.0" not in listen_ips:
|
||||||
|
eps = {k: v for k, v in eps if k in listen_ips}
|
||||||
|
|
||||||
default_route = None
|
default_route = None
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os
|
|||||||
|
|
||||||
from .util import Cooldown
|
from .util import Cooldown
|
||||||
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
class ThumbCli(object):
|
class ThumbCli(object):
|
||||||
@@ -36,7 +37,7 @@ class ThumbCli(object):
|
|||||||
tpath = thumb_path(histpath, rem, mtime, fmt)
|
tpath = thumb_path(histpath, rem, mtime, fmt)
|
||||||
ret = None
|
ret = None
|
||||||
try:
|
try:
|
||||||
st = os.stat(tpath)
|
st = bos.stat(tpath)
|
||||||
if st.st_size:
|
if st.st_size:
|
||||||
ret = tpath
|
ret = tpath
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import threading
|
|||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
from .__init__ import PY2, unicode
|
from .__init__ import PY2, unicode
|
||||||
from .util import fsenc, runcmd, Queue, Cooldown, BytesIO, min_ex
|
from .util import fsenc, vsplit, runcmd, Queue, Cooldown, BytesIO, min_ex
|
||||||
|
from .bos import bos
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||||
|
|
||||||
|
|
||||||
@@ -73,12 +74,7 @@ def thumb_path(histpath, rem, mtime, fmt):
|
|||||||
# base16 = 16 = 256
|
# base16 = 16 = 256
|
||||||
# b64-lc = 38 = 1444
|
# b64-lc = 38 = 1444
|
||||||
# base64 = 64 = 4096
|
# base64 = 64 = 4096
|
||||||
try:
|
rd, fn = vsplit(rem)
|
||||||
rd, fn = rem.rsplit("/", 1)
|
|
||||||
except:
|
|
||||||
rd = ""
|
|
||||||
fn = rem
|
|
||||||
|
|
||||||
if rd:
|
if rd:
|
||||||
h = hashlib.sha512(fsenc(rd)).digest()
|
h = hashlib.sha512(fsenc(rd)).digest()
|
||||||
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
@@ -121,10 +117,10 @@ class ThumbSrv(object):
|
|||||||
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
||||||
missing = []
|
missing = []
|
||||||
if not HAVE_FFMPEG:
|
if not HAVE_FFMPEG:
|
||||||
missing.append("ffmpeg")
|
missing.append("FFmpeg")
|
||||||
|
|
||||||
if not HAVE_FFPROBE:
|
if not HAVE_FFPROBE:
|
||||||
missing.append("ffprobe")
|
missing.append("FFprobe")
|
||||||
|
|
||||||
msg = "cannot create video thumbnails because some of the required programs are not available: "
|
msg = "cannot create video thumbnails because some of the required programs are not available: "
|
||||||
msg += ", ".join(missing)
|
msg += ", ".join(missing)
|
||||||
@@ -159,13 +155,10 @@ class ThumbSrv(object):
|
|||||||
self.log("wait {}".format(tpath))
|
self.log("wait {}".format(tpath))
|
||||||
except:
|
except:
|
||||||
thdir = os.path.dirname(tpath)
|
thdir = os.path.dirname(tpath)
|
||||||
try:
|
bos.makedirs(thdir)
|
||||||
os.makedirs(thdir)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
inf_path = os.path.join(thdir, "dir.txt")
|
inf_path = os.path.join(thdir, "dir.txt")
|
||||||
if not os.path.exists(inf_path):
|
if not bos.path.exists(inf_path):
|
||||||
with open(inf_path, "wb") as f:
|
with open(inf_path, "wb") as f:
|
||||||
f.write(fsenc(os.path.dirname(abspath)))
|
f.write(fsenc(os.path.dirname(abspath)))
|
||||||
|
|
||||||
@@ -185,7 +178,7 @@ class ThumbSrv(object):
|
|||||||
cond.wait(3)
|
cond.wait(3)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = os.stat(tpath)
|
st = bos.stat(tpath)
|
||||||
if st.st_size:
|
if st.st_size:
|
||||||
return tpath
|
return tpath
|
||||||
except:
|
except:
|
||||||
@@ -202,7 +195,7 @@ class ThumbSrv(object):
|
|||||||
abspath, tpath = task
|
abspath, tpath = task
|
||||||
ext = abspath.split(".")[-1].lower()
|
ext = abspath.split(".")[-1].lower()
|
||||||
fun = None
|
fun = None
|
||||||
if not os.path.exists(tpath):
|
if not bos.path.exists(tpath):
|
||||||
if ext in FMT_PIL:
|
if ext in FMT_PIL:
|
||||||
fun = self.conv_pil
|
fun = self.conv_pil
|
||||||
elif ext in FMT_FF:
|
elif ext in FMT_FF:
|
||||||
@@ -313,7 +306,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
|
|
||||||
ret, sout, serr = runcmd(*cmd)
|
ret, sout, serr = runcmd(cmd)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
msg = ["ff: {}".format(x) for x in serr.split("\n")]
|
msg = ["ff: {}".format(x) for x in serr.split("\n")]
|
||||||
self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30")
|
self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30")
|
||||||
@@ -328,7 +321,7 @@ class ThumbSrv(object):
|
|||||||
p1 = os.path.dirname(tdir)
|
p1 = os.path.dirname(tdir)
|
||||||
p2 = os.path.dirname(p1)
|
p2 = os.path.dirname(p1)
|
||||||
for dp in [tdir, p1, p2]:
|
for dp in [tdir, p1, p2]:
|
||||||
os.utime(fsenc(dp), (ts, ts))
|
bos.utime(dp, (ts, ts))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -355,7 +348,7 @@ class ThumbSrv(object):
|
|||||||
prev_b64 = None
|
prev_b64 = None
|
||||||
prev_fp = None
|
prev_fp = None
|
||||||
try:
|
try:
|
||||||
ents = os.listdir(thumbpath)
|
ents = bos.listdir(thumbpath)
|
||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -366,7 +359,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
# "top" or b64 prefix/full (a folder)
|
# "top" or b64 prefix/full (a folder)
|
||||||
if len(f) <= 3 or len(f) == 24:
|
if len(f) <= 3 or len(f) == 24:
|
||||||
age = now - os.path.getmtime(fp)
|
age = now - bos.path.getmtime(fp)
|
||||||
if age > maxage:
|
if age > maxage:
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
safe = True
|
safe = True
|
||||||
@@ -398,7 +391,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
if b64 == prev_b64:
|
if b64 == prev_b64:
|
||||||
self.log("rm replaced [{}]".format(fp))
|
self.log("rm replaced [{}]".format(fp))
|
||||||
os.unlink(prev_fp)
|
bos.unlink(prev_fp)
|
||||||
|
|
||||||
prev_b64 = b64
|
prev_b64 = b64
|
||||||
prev_fp = fp
|
prev_fp = fp
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .__init__ import unicode
|
||||||
from .util import s3dec, Pebkac, min_ex
|
from .util import s3dec, Pebkac, min_ex
|
||||||
|
from .bos import bos
|
||||||
from .up2k import up2k_wark_from_hashlist
|
from .up2k import up2k_wark_from_hashlist
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ class U2idx(object):
|
|||||||
|
|
||||||
histpath = self.asrv.vfs.histtab[ptop]
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
db_path = os.path.join(histpath, "up2k.db")
|
db_path = os.path.join(histpath, "up2k.db")
|
||||||
if not os.path.exists(db_path):
|
if not bos.path.exists(db_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
cur = sqlite3.connect(db_path, 2).cursor()
|
cur = sqlite3.connect(db_path, 2).cursor()
|
||||||
@@ -90,6 +92,8 @@ class U2idx(object):
|
|||||||
mt_ctr = 0
|
mt_ctr = 0
|
||||||
mt_keycmp = "substr(up.w,1,16)"
|
mt_keycmp = "substr(up.w,1,16)"
|
||||||
mt_keycmp2 = None
|
mt_keycmp2 = None
|
||||||
|
ptn_lc = re.compile(r" (mt[0-9]+\.v) ([=<!>]+) \? $")
|
||||||
|
ptn_lcv = re.compile(r"[a-zA-Z]")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
uq = uq.strip()
|
uq = uq.strip()
|
||||||
@@ -182,6 +186,21 @@ class U2idx(object):
|
|||||||
va.append(v)
|
va.append(v)
|
||||||
is_key = True
|
is_key = True
|
||||||
|
|
||||||
|
# lowercase tag searches
|
||||||
|
m = ptn_lc.search(q)
|
||||||
|
if not m or not ptn_lcv.search(unicode(v)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
va.pop()
|
||||||
|
va.append(v.lower())
|
||||||
|
q = q[: m.start()]
|
||||||
|
|
||||||
|
field, oper = m.groups()
|
||||||
|
if oper in ["=", "=="]:
|
||||||
|
q += " {} like ? ".format(field)
|
||||||
|
else:
|
||||||
|
q += " lower({}) {} ? ".format(field, oper)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, joins + "where " + q, va)
|
return self.run_query(vols, joins + "where " + q, va)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|||||||
@@ -23,15 +23,20 @@ from .util import (
|
|||||||
ProgressPrinter,
|
ProgressPrinter,
|
||||||
fsdec,
|
fsdec,
|
||||||
fsenc,
|
fsenc,
|
||||||
|
absreal,
|
||||||
sanitize_fn,
|
sanitize_fn,
|
||||||
ren_open,
|
ren_open,
|
||||||
atomic_move,
|
atomic_move,
|
||||||
|
vsplit,
|
||||||
s3enc,
|
s3enc,
|
||||||
s3dec,
|
s3dec,
|
||||||
|
rmdirs,
|
||||||
statdir,
|
statdir,
|
||||||
s2hms,
|
s2hms,
|
||||||
min_ex,
|
min_ex,
|
||||||
)
|
)
|
||||||
|
from .bos import bos
|
||||||
|
from .authsrv import AuthSrv
|
||||||
from .mtag import MTag, MParser
|
from .mtag import MTag, MParser
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -44,16 +49,9 @@ DB_VER = 4
|
|||||||
|
|
||||||
|
|
||||||
class Up2k(object):
|
class Up2k(object):
|
||||||
"""
|
|
||||||
TODO:
|
|
||||||
* documentation
|
|
||||||
* registry persistence
|
|
||||||
* ~/.config flatfiles for active jobs
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, hub):
|
def __init__(self, hub):
|
||||||
self.hub = hub
|
self.hub = hub
|
||||||
self.asrv = hub.asrv
|
self.asrv = hub.asrv # type: AuthSrv
|
||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
@@ -67,6 +65,7 @@ class Up2k(object):
|
|||||||
self.n_hashq = 0
|
self.n_hashq = 0
|
||||||
self.n_tagq = 0
|
self.n_tagq = 0
|
||||||
self.volstate = {}
|
self.volstate = {}
|
||||||
|
self.need_rescan = {}
|
||||||
self.registry = {}
|
self.registry = {}
|
||||||
self.entags = {}
|
self.entags = {}
|
||||||
self.flags = {}
|
self.flags = {}
|
||||||
@@ -101,17 +100,16 @@ class Up2k(object):
|
|||||||
|
|
||||||
if self.args.no_fastboot:
|
if self.args.no_fastboot:
|
||||||
self.deferred_init()
|
self.deferred_init()
|
||||||
else:
|
|
||||||
t = threading.Thread(
|
|
||||||
target=self.deferred_init, name="up2k-deferred-init", args=(0.5,)
|
|
||||||
)
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
def deferred_init(self, wait=0):
|
def init_vols(self):
|
||||||
if wait:
|
if self.args.no_fastboot:
|
||||||
time.sleep(wait)
|
return
|
||||||
|
|
||||||
|
t = threading.Thread(target=self.deferred_init, name="up2k-deferred-init")
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def deferred_init(self):
|
||||||
all_vols = self.asrv.vfs.all_vols
|
all_vols = self.asrv.vfs.all_vols
|
||||||
have_e2d = self.init_indexes(all_vols)
|
have_e2d = self.init_indexes(all_vols)
|
||||||
|
|
||||||
@@ -124,6 +122,10 @@ class Up2k(object):
|
|||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self._sched_rescan, name="up2k-rescan")
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
if self.mtag:
|
if self.mtag:
|
||||||
thr = threading.Thread(target=self._tagger, name="up2k-tagger")
|
thr = threading.Thread(target=self._tagger, name="up2k-tagger")
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
@@ -173,6 +175,38 @@ class Up2k(object):
|
|||||||
t.start()
|
t.start()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _sched_rescan(self):
|
||||||
|
maxage = self.args.re_maxage
|
||||||
|
volage = {}
|
||||||
|
while True:
|
||||||
|
time.sleep(self.args.re_int)
|
||||||
|
now = time.time()
|
||||||
|
vpaths = list(sorted(self.asrv.vfs.all_vols.keys()))
|
||||||
|
with self.mutex:
|
||||||
|
if maxage:
|
||||||
|
for vp in vpaths:
|
||||||
|
if vp not in volage:
|
||||||
|
volage[vp] = now
|
||||||
|
|
||||||
|
if now - volage[vp] >= maxage:
|
||||||
|
self.need_rescan[vp] = 1
|
||||||
|
|
||||||
|
if not self.need_rescan:
|
||||||
|
continue
|
||||||
|
|
||||||
|
vols = list(sorted(self.need_rescan.keys()))
|
||||||
|
self.need_rescan = {}
|
||||||
|
|
||||||
|
err = self.rescan(self.asrv.vfs.all_vols, vols)
|
||||||
|
if err:
|
||||||
|
for v in vols:
|
||||||
|
self.need_rescan[v] = True
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
for v in vols:
|
||||||
|
volage[v] = now
|
||||||
|
|
||||||
def _vis_job_progress(self, job):
|
def _vis_job_progress(self, job):
|
||||||
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
@@ -218,7 +252,7 @@ class Up2k(object):
|
|||||||
# only need to protect register_vpath but all in one go feels right
|
# only need to protect register_vpath but all in one go feels right
|
||||||
for vol in vols:
|
for vol in vols:
|
||||||
try:
|
try:
|
||||||
os.listdir(vol.realpath)
|
bos.listdir(vol.realpath)
|
||||||
except:
|
except:
|
||||||
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
|
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
|
||||||
self.log("cannot access " + vol.realpath, c=1)
|
self.log("cannot access " + vol.realpath, c=1)
|
||||||
@@ -304,7 +338,7 @@ class Up2k(object):
|
|||||||
self.log(msg.format(len(vols), time.time() - t0))
|
self.log(msg.format(len(vols), time.time() - t0))
|
||||||
|
|
||||||
if needed_mutagen:
|
if needed_mutagen:
|
||||||
msg = "could not read tags because no backends are available (mutagen or ffprobe)"
|
msg = "could not read tags because no backends are available (Mutagen or FFprobe)"
|
||||||
self.log(msg, c=1)
|
self.log(msg, c=1)
|
||||||
|
|
||||||
thr = None
|
thr = None
|
||||||
@@ -356,14 +390,14 @@ class Up2k(object):
|
|||||||
|
|
||||||
reg = {}
|
reg = {}
|
||||||
path = os.path.join(histpath, "up2k.snap")
|
path = os.path.join(histpath, "up2k.snap")
|
||||||
if "e2d" in flags and os.path.exists(path):
|
if "e2d" in flags and bos.path.exists(path):
|
||||||
with gzip.GzipFile(path, "rb") as f:
|
with gzip.GzipFile(path, "rb") as f:
|
||||||
j = f.read().decode("utf-8")
|
j = f.read().decode("utf-8")
|
||||||
|
|
||||||
reg2 = json.loads(j)
|
reg2 = json.loads(j)
|
||||||
for k, job in reg2.items():
|
for k, job in reg2.items():
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
if os.path.exists(fsenc(path)):
|
if bos.path.exists(path):
|
||||||
reg[k] = job
|
reg[k] = job
|
||||||
job["poke"] = time.time()
|
job["poke"] = time.time()
|
||||||
else:
|
else:
|
||||||
@@ -378,10 +412,7 @@ class Up2k(object):
|
|||||||
if not HAVE_SQLITE3 or "e2d" not in flags or "d2d" in flags:
|
if not HAVE_SQLITE3 or "e2d" not in flags or "d2d" in flags:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
bos.makedirs(histpath)
|
||||||
os.makedirs(histpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cur = self._open_db(db_path)
|
cur = self._open_db(db_path)
|
||||||
@@ -420,14 +451,7 @@ class Up2k(object):
|
|||||||
return True, n_add or n_rm or do_vac
|
return True, n_add or n_rm or do_vac
|
||||||
|
|
||||||
def _build_dir(self, dbw, top, excl, cdir, nohash, seen):
|
def _build_dir(self, dbw, top, excl, cdir, nohash, seen):
|
||||||
rcdir = cdir
|
rcdir = absreal(cdir) # a bit expensive but worth
|
||||||
if not ANYWIN:
|
|
||||||
try:
|
|
||||||
# a bit expensive but worth
|
|
||||||
rcdir = os.path.realpath(cdir)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if rcdir in seen:
|
if rcdir in seen:
|
||||||
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}"
|
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}"
|
||||||
self.log(m.format(seen[-1], rcdir, cdir), 3)
|
self.log(m.format(seen[-1], rcdir, cdir), 3)
|
||||||
@@ -523,7 +547,7 @@ class Up2k(object):
|
|||||||
# almost zero overhead dw
|
# almost zero overhead dw
|
||||||
self.pp.msg = "b{} {}".format(nfiles - nchecked, abspath)
|
self.pp.msg = "b{} {}".format(nfiles - nchecked, abspath)
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(fsenc(abspath)):
|
if not bos.path.exists(abspath):
|
||||||
rm.append([drd, dfn])
|
rm.append([drd, dfn])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log("stat-rm: {} @ [{}]".format(repr(ex), abspath))
|
self.log("stat-rm: {} @ [{}]".format(repr(ex), abspath))
|
||||||
@@ -596,7 +620,7 @@ class Up2k(object):
|
|||||||
c2 = conn.cursor()
|
c2 = conn.cursor()
|
||||||
c3 = conn.cursor()
|
c3 = conn.cursor()
|
||||||
n_left = cur.execute("select count(w) from up").fetchone()[0]
|
n_left = cur.execute("select count(w) from up").fetchone()[0]
|
||||||
for w, rd, fn in cur.execute("select w, rd, fn from up"):
|
for w, rd, fn in cur.execute("select w, rd, fn from up order by rd, fn"):
|
||||||
n_left -= 1
|
n_left -= 1
|
||||||
q = "select w from mt where w = ?"
|
q = "select w from mt where w = ?"
|
||||||
if c2.execute(q, (w[:16],)).fetchone():
|
if c2.execute(q, (w[:16],)).fetchone():
|
||||||
@@ -911,7 +935,7 @@ class Up2k(object):
|
|||||||
# x.set_trace_callback(trace)
|
# x.set_trace_callback(trace)
|
||||||
|
|
||||||
def _open_db(self, db_path):
|
def _open_db(self, db_path):
|
||||||
existed = os.path.exists(db_path)
|
existed = bos.path.exists(db_path)
|
||||||
cur = self._orz(db_path)
|
cur = self._orz(db_path)
|
||||||
ver = self._read_ver(cur)
|
ver = self._read_ver(cur)
|
||||||
if not existed and ver is None:
|
if not existed and ver is None:
|
||||||
@@ -929,19 +953,38 @@ class Up2k(object):
|
|||||||
m = "database is version {}, this copyparty only supports versions <= {}"
|
m = "database is version {}, this copyparty only supports versions <= {}"
|
||||||
raise Exception(m.format(ver, DB_VER))
|
raise Exception(m.format(ver, DB_VER))
|
||||||
|
|
||||||
bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver)
|
|
||||||
db = cur.connection
|
|
||||||
cur.close()
|
|
||||||
db.close()
|
|
||||||
msg = "creating new DB (old is bad); backup: {}"
|
msg = "creating new DB (old is bad); backup: {}"
|
||||||
if ver:
|
if ver:
|
||||||
msg = "creating new DB (too old to upgrade); backup: {}"
|
msg = "creating new DB (too old to upgrade); backup: {}"
|
||||||
|
|
||||||
self.log(msg.format(bak))
|
cur = self._backup_db(db_path, cur, ver, msg)
|
||||||
os.rename(fsenc(db_path), fsenc(bak))
|
db = cur.connection
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
bos.unlink(db_path)
|
||||||
return self._create_db(db_path, None)
|
return self._create_db(db_path, None)
|
||||||
|
|
||||||
|
def _backup_db(self, db_path, cur, ver, msg):
|
||||||
|
bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver)
|
||||||
|
self.log(msg + bak)
|
||||||
|
try:
|
||||||
|
c2 = sqlite3.connect(bak)
|
||||||
|
with c2:
|
||||||
|
cur.connection.backup(c2)
|
||||||
|
return cur
|
||||||
|
except:
|
||||||
|
m = "native sqlite3 backup failed; using fallback method:\n"
|
||||||
|
self.log(m + min_ex())
|
||||||
|
finally:
|
||||||
|
c2.close()
|
||||||
|
|
||||||
|
db = cur.connection
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
shutil.copy2(fsenc(db_path), fsenc(bak))
|
||||||
|
return self._orz(db_path)
|
||||||
|
|
||||||
def _read_ver(self, cur):
|
def _read_ver(self, cur):
|
||||||
for tab in ["ki", "kv"]:
|
for tab in ["ki", "kv"]:
|
||||||
try:
|
try:
|
||||||
@@ -1014,7 +1057,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
dp_abs = "/".join([cj["ptop"], dp_dir, dp_fn])
|
dp_abs = "/".join([cj["ptop"], dp_dir, dp_fn])
|
||||||
# relying on path.exists to return false on broken symlinks
|
# relying on path.exists to return false on broken symlinks
|
||||||
if os.path.exists(fsenc(dp_abs)):
|
if bos.path.exists(dp_abs):
|
||||||
job = {
|
job = {
|
||||||
"name": dp_fn,
|
"name": dp_fn,
|
||||||
"prel": dp_dir,
|
"prel": dp_dir,
|
||||||
@@ -1038,7 +1081,7 @@ class Up2k(object):
|
|||||||
for fn in names:
|
for fn in names:
|
||||||
path = os.path.join(job["ptop"], job["prel"], fn)
|
path = os.path.join(job["ptop"], job["prel"], fn)
|
||||||
try:
|
try:
|
||||||
if os.path.getsize(fsenc(path)) > 0:
|
if bos.path.getsize(path) > 0:
|
||||||
# upload completed or both present
|
# upload completed or both present
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
@@ -1072,9 +1115,14 @@ class Up2k(object):
|
|||||||
job["name"] = self._untaken(pdir, cj["name"], now, cj["addr"])
|
job["name"] = self._untaken(pdir, cj["name"], now, cj["addr"])
|
||||||
dst = os.path.join(job["ptop"], job["prel"], job["name"])
|
dst = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
if not self.args.nw:
|
if not self.args.nw:
|
||||||
os.unlink(fsenc(dst)) # TODO ed pls
|
bos.unlink(dst) # TODO ed pls
|
||||||
self._symlink(src, dst)
|
self._symlink(src, dst)
|
||||||
|
|
||||||
|
if cur:
|
||||||
|
a = [cj[x] for x in "prel name lmod size".split()]
|
||||||
|
self.db_add(cur, wark, *a)
|
||||||
|
cur.connection.commit()
|
||||||
|
|
||||||
if not job:
|
if not job:
|
||||||
job = {
|
job = {
|
||||||
"wark": wark,
|
"wark": wark,
|
||||||
@@ -1124,17 +1172,18 @@ class Up2k(object):
|
|||||||
with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as f:
|
with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as f:
|
||||||
return f["orz"][1]
|
return f["orz"][1]
|
||||||
|
|
||||||
def _symlink(self, src, dst):
|
def _symlink(self, src, dst, verbose=True):
|
||||||
# TODO store this in linktab so we never delete src if there are links to it
|
if verbose:
|
||||||
self.log("linking dupe:\n {0}\n {1}".format(src, dst))
|
self.log("linking dupe:\n {0}\n {1}".format(src, dst))
|
||||||
|
|
||||||
if self.args.nw:
|
if self.args.nw:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lsrc = src
|
lsrc = src
|
||||||
ldst = dst
|
ldst = dst
|
||||||
fs1 = os.stat(fsenc(os.path.split(src)[0])).st_dev
|
fs1 = bos.stat(os.path.dirname(src)).st_dev
|
||||||
fs2 = os.stat(fsenc(os.path.split(dst)[0])).st_dev
|
fs2 = bos.stat(os.path.dirname(dst)).st_dev
|
||||||
if fs1 == 0:
|
if fs1 == 0:
|
||||||
# py2 on winxp or other unsupported combination
|
# py2 on winxp or other unsupported combination
|
||||||
raise OSError()
|
raise OSError()
|
||||||
@@ -1217,15 +1266,8 @@ class Up2k(object):
|
|||||||
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))]
|
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))]
|
||||||
self.lastmod_q.put(a)
|
self.lastmod_q.put(a)
|
||||||
|
|
||||||
# legit api sware 2 me mum
|
a = [job[x] for x in "ptop wark prel name lmod size".split()]
|
||||||
if self.idx_wark(
|
if self.idx_wark(*a):
|
||||||
job["ptop"],
|
|
||||||
job["wark"],
|
|
||||||
job["prel"],
|
|
||||||
job["name"],
|
|
||||||
job["lmod"],
|
|
||||||
job["size"],
|
|
||||||
):
|
|
||||||
del self.registry[ptop][wark]
|
del self.registry[ptop][wark]
|
||||||
# in-memory registry is reserved for unfinished uploads
|
# in-memory registry is reserved for unfinished uploads
|
||||||
|
|
||||||
@@ -1237,7 +1279,7 @@ class Up2k(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self.db_rm(cur, rd, fn)
|
self.db_rm(cur, rd, fn)
|
||||||
self.db_add(cur, wark, rd, fn, int(lmod), sz)
|
self.db_add(cur, wark, rd, fn, lmod, sz)
|
||||||
cur.connection.commit()
|
cur.connection.commit()
|
||||||
|
|
||||||
if "e2t" in self.flags[ptop]:
|
if "e2t" in self.flags[ptop]:
|
||||||
@@ -1260,9 +1302,251 @@ class Up2k(object):
|
|||||||
db.execute(sql, v)
|
db.execute(sql, v)
|
||||||
except:
|
except:
|
||||||
rd, fn = s3enc(self.mem_cur, rd, fn)
|
rd, fn = s3enc(self.mem_cur, rd, fn)
|
||||||
v = (wark, ts, sz, rd, fn)
|
v = (wark, int(ts), sz, rd, fn)
|
||||||
db.execute(sql, v)
|
db.execute(sql, v)
|
||||||
|
|
||||||
|
def handle_rm(self, uname, vpath):
|
||||||
|
permsets = [[True, False, False, True]]
|
||||||
|
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||||
|
ptop = vn.realpath
|
||||||
|
atop = vn.canonical(rem)
|
||||||
|
adir, fn = os.path.split(atop)
|
||||||
|
st = bos.lstat(atop)
|
||||||
|
scandir = not self.args.no_scandir
|
||||||
|
if stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
|
||||||
|
dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||||
|
dbv, vrem = dbv.get_dbv(vrem)
|
||||||
|
g = [[dbv, vrem, os.path.dirname(vpath), adir, [[fn, 0]], [], []]]
|
||||||
|
else:
|
||||||
|
g = vn.walk("", rem, [], uname, permsets, True, scandir, True)
|
||||||
|
|
||||||
|
n_files = 0
|
||||||
|
for dbv, vrem, _, adir, files, rd, vd in g:
|
||||||
|
for fn in [x[0] for x in files]:
|
||||||
|
n_files += 1
|
||||||
|
abspath = os.path.join(adir, fn)
|
||||||
|
vpath = "{}/{}".format(vrem, fn).strip("/")
|
||||||
|
self.log("rm {}\n {}".format(vpath, abspath))
|
||||||
|
_ = dbv.get(vrem, uname, *permsets[0])
|
||||||
|
with self.mutex:
|
||||||
|
try:
|
||||||
|
ptop = dbv.realpath
|
||||||
|
cur, wark, _, _ = self._find_from_vpath(ptop, vrem)
|
||||||
|
self._forget_file(ptop, vpath, cur, wark)
|
||||||
|
finally:
|
||||||
|
cur.connection.commit()
|
||||||
|
|
||||||
|
bos.unlink(abspath)
|
||||||
|
|
||||||
|
rm = rmdirs(self.log_func, scandir, True, atop)
|
||||||
|
ok = len(rm[0])
|
||||||
|
ng = len(rm[1])
|
||||||
|
return "deleted {} files (and {}/{} folders)".format(n_files, ok, ok + ng)
|
||||||
|
|
||||||
|
def handle_mv(self, uname, svp, dvp):
|
||||||
|
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True)
|
||||||
|
svn, srem = svn.get_dbv(srem)
|
||||||
|
sabs = svn.canonical(srem, False)
|
||||||
|
|
||||||
|
if not srem:
|
||||||
|
raise Pebkac(400, "mv: cannot move a mountpoint")
|
||||||
|
|
||||||
|
st = bos.stat(sabs)
|
||||||
|
if stat.S_ISREG(st.st_mode):
|
||||||
|
return self._mv_file(uname, svp, dvp)
|
||||||
|
|
||||||
|
jail = svn.get_dbv(srem)[0]
|
||||||
|
permsets = [[True, False, True]]
|
||||||
|
scandir = not self.args.no_scandir
|
||||||
|
|
||||||
|
# following symlinks is too scary
|
||||||
|
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||||
|
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||||
|
if dbv != jail:
|
||||||
|
# fail early (prevent partial moves)
|
||||||
|
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||||
|
|
||||||
|
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||||
|
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||||
|
if dbv != jail:
|
||||||
|
# the actual check (avoid toctou)
|
||||||
|
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||||
|
|
||||||
|
for fn in files:
|
||||||
|
svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x)
|
||||||
|
if not svpf.startswith(svp + "/"): # assert
|
||||||
|
raise Pebkac(500, "mv: bug at {}, top {}".format(svpf, svp))
|
||||||
|
|
||||||
|
dvpf = dvp + svpf[len(svp) :]
|
||||||
|
self._mv_file(uname, svpf, dvpf)
|
||||||
|
|
||||||
|
rmdirs(self.log_func, scandir, True, sabs)
|
||||||
|
return "k"
|
||||||
|
|
||||||
|
def _mv_file(self, uname, svp, dvp):
|
||||||
|
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True)
|
||||||
|
svn, srem = svn.get_dbv(srem)
|
||||||
|
|
||||||
|
dvn, drem = self.asrv.vfs.get(dvp, uname, False, True)
|
||||||
|
dvn, drem = dvn.get_dbv(drem)
|
||||||
|
|
||||||
|
sabs = svn.canonical(srem, False)
|
||||||
|
dabs = dvn.canonical(drem)
|
||||||
|
drd, dfn = vsplit(drem)
|
||||||
|
|
||||||
|
if bos.path.exists(dabs):
|
||||||
|
raise Pebkac(400, "mv2: target file exists")
|
||||||
|
|
||||||
|
bos.makedirs(os.path.dirname(dabs))
|
||||||
|
|
||||||
|
if bos.path.islink(sabs):
|
||||||
|
dlabs = absreal(sabs)
|
||||||
|
m = "moving symlink from [{}] to [{}], target [{}]"
|
||||||
|
self.log(m.format(sabs, dabs, dlabs))
|
||||||
|
os.unlink(sabs)
|
||||||
|
self._symlink(dlabs, dabs, False)
|
||||||
|
|
||||||
|
# folders are too scary, schedule rescan of both vols
|
||||||
|
self.need_rescan[svn.vpath] = 1
|
||||||
|
self.need_rescan[dvn.vpath] = 1
|
||||||
|
return "k"
|
||||||
|
|
||||||
|
c1, w, ftime, fsize = self._find_from_vpath(svn.realpath, srem)
|
||||||
|
c2 = self.cur.get(dvn.realpath)
|
||||||
|
|
||||||
|
if ftime is None:
|
||||||
|
st = bos.stat(sabs)
|
||||||
|
ftime = st.st_mtime
|
||||||
|
fsize = st.st_size
|
||||||
|
|
||||||
|
if w:
|
||||||
|
if c2:
|
||||||
|
self._copy_tags(c1, c2, w)
|
||||||
|
|
||||||
|
self._forget_file(svn.realpath, srem, c1, w)
|
||||||
|
self._relink(w, svn.realpath, srem, dabs)
|
||||||
|
c1.connection.commit()
|
||||||
|
|
||||||
|
if c2:
|
||||||
|
self.db_add(c2, w, drd, dfn, ftime, fsize)
|
||||||
|
c2.connection.commit()
|
||||||
|
else:
|
||||||
|
self.log("not found in src db: [{}]".format(svp))
|
||||||
|
|
||||||
|
bos.rename(sabs, dabs)
|
||||||
|
return "k"
|
||||||
|
|
||||||
|
def _copy_tags(self, csrc, cdst, wark):
|
||||||
|
"""copy all tags for wark from src-db to dst-db"""
|
||||||
|
w = wark[:16]
|
||||||
|
|
||||||
|
if cdst.execute("select * from mt where w=? limit 1", (w,)).fetchone():
|
||||||
|
return # existing tags in dest db
|
||||||
|
|
||||||
|
for _, k, v in csrc.execute("select * from mt where w=?", (w,)):
|
||||||
|
cdst.execute("insert into mt values(?,?,?)", (w, k, v))
|
||||||
|
|
||||||
|
def _find_from_vpath(self, ptop, vrem):
|
||||||
|
cur = self.cur.get(ptop)
|
||||||
|
if not cur:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
rd, fn = vsplit(vrem)
|
||||||
|
q = "select w, mt, sz from up where rd=? and fn=? limit 1"
|
||||||
|
try:
|
||||||
|
c = cur.execute(q, (rd, fn))
|
||||||
|
except:
|
||||||
|
c = cur.execute(q, s3enc(self.mem_cur, rd, fn))
|
||||||
|
|
||||||
|
hit = c.fetchone()
|
||||||
|
if hit:
|
||||||
|
wark, ftime, fsize = hit
|
||||||
|
return cur, wark, ftime, fsize
|
||||||
|
return cur, None, None, None
|
||||||
|
|
||||||
|
def _forget_file(self, ptop, vrem, cur, wark):
|
||||||
|
"""forgets file in db, fixes symlinks, does not delete"""
|
||||||
|
srd, sfn = vsplit(vrem)
|
||||||
|
self.log("forgetting {}".format(vrem))
|
||||||
|
if wark:
|
||||||
|
self.log("found {} in db".format(wark))
|
||||||
|
self._relink(wark, ptop, vrem, None)
|
||||||
|
|
||||||
|
q = "delete from mt where w=?"
|
||||||
|
cur.execute(q, (wark[:16],))
|
||||||
|
self.db_rm(cur, srd, sfn)
|
||||||
|
|
||||||
|
reg = self.registry.get(ptop)
|
||||||
|
if reg:
|
||||||
|
if not wark:
|
||||||
|
wark = [
|
||||||
|
x
|
||||||
|
for x, y in reg.items()
|
||||||
|
if fn in [y["name"], y.get("tnam")] and y["prel"] == vrem
|
||||||
|
]
|
||||||
|
|
||||||
|
if wark and wark in reg:
|
||||||
|
m = "forgetting partial upload {} ({})"
|
||||||
|
p = self._vis_job_progress(wark)
|
||||||
|
self.log(m.format(wark, p))
|
||||||
|
del reg[wark]
|
||||||
|
|
||||||
|
def _relink(self, wark, sptop, srem, dabs):
|
||||||
|
"""
|
||||||
|
update symlinks from file at svn/srem to dabs (rename),
|
||||||
|
or to first remaining full if no dabs (delete)
|
||||||
|
"""
|
||||||
|
dupes = []
|
||||||
|
sabs = os.path.join(sptop, srem)
|
||||||
|
q = "select rd, fn from up where substr(w,1,16)=? and w=?"
|
||||||
|
for ptop, cur in self.cur.items():
|
||||||
|
for rd, fn in cur.execute(q, (wark[:16], wark)):
|
||||||
|
if rd.startswith("//") or fn.startswith("//"):
|
||||||
|
rd, fn = s3dec(rd, fn)
|
||||||
|
|
||||||
|
dvrem = "/".join([rd, fn]).strip("/")
|
||||||
|
if ptop != sptop or srem != dvrem:
|
||||||
|
dupes.append([ptop, dvrem])
|
||||||
|
self.log("found {} dupe: [{}] {}".format(wark, ptop, dvrem))
|
||||||
|
|
||||||
|
if not dupes:
|
||||||
|
return
|
||||||
|
|
||||||
|
full = {}
|
||||||
|
links = {}
|
||||||
|
for ptop, vp in dupes:
|
||||||
|
ap = os.path.join(ptop, vp)
|
||||||
|
try:
|
||||||
|
d = links if bos.path.islink(ap) else full
|
||||||
|
d[ap] = [ptop, vp]
|
||||||
|
except:
|
||||||
|
self.log("relink: not found: [{}]".format(ap))
|
||||||
|
|
||||||
|
if not dabs and not full and links:
|
||||||
|
# deleting final remaining full copy; swap it with a symlink
|
||||||
|
slabs = list(sorted(links.keys()))[0]
|
||||||
|
ptop, rem = links.pop(slabs)
|
||||||
|
self.log("linkswap [{}] and [{}]".format(sabs, dabs))
|
||||||
|
bos.unlink(slabs)
|
||||||
|
bos.rename(sabs, slabs)
|
||||||
|
self._symlink(slabs, sabs, False)
|
||||||
|
full[slabs] = [ptop, rem]
|
||||||
|
|
||||||
|
if not dabs:
|
||||||
|
dabs = list(sorted(full.keys()))[0]
|
||||||
|
|
||||||
|
for alink in links.keys():
|
||||||
|
try:
|
||||||
|
if alink != sabs and absreal(alink) != sabs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.log("relinking [{}] to [{}]".format(alink, dabs))
|
||||||
|
bos.unlink(alink)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._symlink(dabs, alink, False)
|
||||||
|
|
||||||
def _get_wark(self, cj):
|
def _get_wark(self, cj):
|
||||||
if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB
|
if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB
|
||||||
raise Pebkac(400, "name or numchunks not according to spec")
|
raise Pebkac(400, "name or numchunks not according to spec")
|
||||||
@@ -1284,7 +1568,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
def _hashlist_from_file(self, path):
|
def _hashlist_from_file(self, path):
|
||||||
pp = self.pp if hasattr(self, "pp") else None
|
pp = self.pp if hasattr(self, "pp") else None
|
||||||
fsz = os.path.getsize(fsenc(path))
|
fsz = bos.path.getsize(path)
|
||||||
csz = up2k_chunksize(fsz)
|
csz = up2k_chunksize(fsz)
|
||||||
ret = []
|
ret = []
|
||||||
with open(fsenc(path), "rb", 512 * 1024) as f:
|
with open(fsenc(path), "rb", 512 * 1024) as f:
|
||||||
@@ -1352,7 +1636,7 @@ class Up2k(object):
|
|||||||
for path, sz, times in ready:
|
for path, sz, times in ready:
|
||||||
self.log("lmod: setting times {} on {}".format(times, path))
|
self.log("lmod: setting times {} on {}".format(times, path))
|
||||||
try:
|
try:
|
||||||
os.utime(fsenc(path), times)
|
bos.utime(path, times)
|
||||||
except:
|
except:
|
||||||
self.log("lmod: failed to utime ({}, {})".format(path, times))
|
self.log("lmod: failed to utime ({}, {})".format(path, times))
|
||||||
|
|
||||||
@@ -1388,13 +1672,13 @@ class Up2k(object):
|
|||||||
try:
|
try:
|
||||||
# remove the filename reservation
|
# remove the filename reservation
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
if os.path.getsize(fsenc(path)) == 0:
|
if bos.path.getsize(path) == 0:
|
||||||
os.unlink(fsenc(path))
|
bos.unlink(path)
|
||||||
|
|
||||||
if len(job["hash"]) == len(job["need"]):
|
if len(job["hash"]) == len(job["need"]):
|
||||||
# PARTIAL is empty, delete that too
|
# PARTIAL is empty, delete that too
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["tnam"])
|
path = os.path.join(job["ptop"], job["prel"], job["tnam"])
|
||||||
os.unlink(fsenc(path))
|
bos.unlink(path)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1402,8 +1686,8 @@ class Up2k(object):
|
|||||||
if not reg:
|
if not reg:
|
||||||
if ptop not in self.snap_prev or self.snap_prev[ptop] is not None:
|
if ptop not in self.snap_prev or self.snap_prev[ptop] is not None:
|
||||||
self.snap_prev[ptop] = None
|
self.snap_prev[ptop] = None
|
||||||
if os.path.exists(fsenc(path)):
|
if bos.path.exists(path):
|
||||||
os.unlink(fsenc(path))
|
bos.unlink(path)
|
||||||
return
|
return
|
||||||
|
|
||||||
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
||||||
@@ -1411,10 +1695,7 @@ class Up2k(object):
|
|||||||
if etag == self.snap_prev.get(ptop):
|
if etag == self.snap_prev.get(ptop):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
bos.makedirs(histpath)
|
||||||
os.makedirs(histpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
path2 = "{}.{}".format(path, os.getpid())
|
path2 = "{}.{}".format(path, os.getpid())
|
||||||
j = json.dumps(reg, indent=2, sort_keys=True).encode("utf-8")
|
j = json.dumps(reg, indent=2, sort_keys=True).encode("utf-8")
|
||||||
@@ -1479,7 +1760,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
abspath = os.path.join(ptop, rd, fn)
|
abspath = os.path.join(ptop, rd, fn)
|
||||||
self.log("hashing " + abspath)
|
self.log("hashing " + abspath)
|
||||||
inf = os.stat(fsenc(abspath))
|
inf = bos.stat(abspath)
|
||||||
hashes = self._hashlist_from_file(abspath)
|
hashes = self._hashlist_from_file(abspath)
|
||||||
wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes)
|
wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes)
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
@@ -1512,7 +1793,7 @@ def up2k_chunksize(filesize):
|
|||||||
|
|
||||||
|
|
||||||
def up2k_wark_from_hashlist(salt, filesize, hashes):
|
def up2k_wark_from_hashlist(salt, filesize, hashes):
|
||||||
""" server-reproducible file identifier, independent of name or location """
|
"""server-reproducible file identifier, independent of name or location"""
|
||||||
ident = [salt, str(filesize)]
|
ident = [salt, str(filesize)]
|
||||||
ident.extend(hashes)
|
ident.extend(hashes)
|
||||||
ident = "\n".join(ident)
|
ident = "\n".join(ident)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import stat
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
import select
|
import select
|
||||||
@@ -758,6 +759,19 @@ def sanitize_fn(fn, ok, bad):
|
|||||||
return fn.strip()
|
return fn.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def absreal(fpath):
|
||||||
|
try:
|
||||||
|
return fsdec(os.path.abspath(os.path.realpath(fsenc(fpath))))
|
||||||
|
except:
|
||||||
|
if not WINDOWS:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# cpython bug introduced in 3.8, still exists in 3.9.1,
|
||||||
|
# some win7sp1 and win10:20H2 boxes cannot realpath a
|
||||||
|
# networked drive letter such as b"n:" or b"n:\\"
|
||||||
|
return os.path.abspath(os.path.realpath(fpath))
|
||||||
|
|
||||||
|
|
||||||
def u8safe(txt):
|
def u8safe(txt):
|
||||||
try:
|
try:
|
||||||
return txt.encode("utf-8", "xmlcharrefreplace").decode("utf-8", "replace")
|
return txt.encode("utf-8", "xmlcharrefreplace").decode("utf-8", "replace")
|
||||||
@@ -815,6 +829,13 @@ def unquotep(txt):
|
|||||||
return w8dec(unq2)
|
return w8dec(unq2)
|
||||||
|
|
||||||
|
|
||||||
|
def vsplit(vpath):
|
||||||
|
if "/" not in vpath:
|
||||||
|
return "", vpath
|
||||||
|
|
||||||
|
return vpath.rsplit("/", 1)
|
||||||
|
|
||||||
|
|
||||||
def w8dec(txt):
|
def w8dec(txt):
|
||||||
"""decodes filesystem-bytes to wtf8"""
|
"""decodes filesystem-bytes to wtf8"""
|
||||||
if PY2:
|
if PY2:
|
||||||
@@ -1014,6 +1035,9 @@ def sendfile_kern(lower, upper, f, s):
|
|||||||
|
|
||||||
|
|
||||||
def statdir(logger, scandir, lstat, top):
|
def statdir(logger, scandir, lstat, top):
|
||||||
|
if lstat and not os.supports_follow_symlinks:
|
||||||
|
scandir = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
btop = fsenc(top)
|
btop = fsenc(top)
|
||||||
if scandir and hasattr(os, "scandir"):
|
if scandir and hasattr(os, "scandir"):
|
||||||
@@ -1038,6 +1062,26 @@ def statdir(logger, scandir, lstat, top):
|
|||||||
logger(src, "{} @ {}".format(repr(ex), top), 1)
|
logger(src, "{} @ {}".format(repr(ex), top), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def rmdirs(logger, scandir, lstat, top):
|
||||||
|
dirs = statdir(logger, scandir, lstat, top)
|
||||||
|
dirs = [x[0] for x in dirs if stat.S_ISDIR(x[1].st_mode)]
|
||||||
|
dirs = [os.path.join(top, x) for x in dirs]
|
||||||
|
ok = []
|
||||||
|
ng = []
|
||||||
|
for d in dirs[::-1]:
|
||||||
|
a, b = rmdirs(logger, scandir, lstat, d)
|
||||||
|
ok += a
|
||||||
|
ng += b
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.rmdir(fsenc(top))
|
||||||
|
ok.append(top)
|
||||||
|
except:
|
||||||
|
ng.append(top)
|
||||||
|
|
||||||
|
return ok, ng
|
||||||
|
|
||||||
|
|
||||||
def unescape_cookie(orig):
|
def unescape_cookie(orig):
|
||||||
# mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn
|
# mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn
|
||||||
ret = ""
|
ret = ""
|
||||||
@@ -1081,7 +1125,7 @@ def guess_mime(url, fallback="application/octet-stream"):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
stdout = stdout.decode("utf-8", "replace")
|
stdout = stdout.decode("utf-8", "replace")
|
||||||
@@ -1089,8 +1133,8 @@ def runcmd(*argv):
|
|||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
def chkcmd(*argv):
|
def chkcmd(argv):
|
||||||
ok, sout, serr = runcmd(*argv)
|
ok, sout, serr = runcmd(argv)
|
||||||
if ok != 0:
|
if ok != 0:
|
||||||
raise Exception(serr)
|
raise Exception(serr)
|
||||||
|
|
||||||
|
|||||||
@@ -25,20 +25,96 @@ html, body {
|
|||||||
body {
|
body {
|
||||||
padding-bottom: 5em;
|
padding-bottom: 5em;
|
||||||
}
|
}
|
||||||
#tt {
|
pre, code, tt {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#tt, #toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
max-width: 34em;
|
max-width: 34em;
|
||||||
background: #222;
|
background: #222;
|
||||||
border: 0 solid #777;
|
border: 0 solid #777;
|
||||||
|
box-shadow: 0 .2em .5em #222;
|
||||||
|
border-radius: .4em;
|
||||||
|
z-index: 9001;
|
||||||
|
}
|
||||||
|
#tt {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
padding: 0 1.3em;
|
padding: 0 1.3em;
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: .1;
|
opacity: .1;
|
||||||
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||||
box-shadow: 0 .2em .5em #222;
|
}
|
||||||
border-radius: .4em;
|
#toast {
|
||||||
z-index: 9001;
|
top: 1.4em;
|
||||||
|
right: -1em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition:
|
||||||
|
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||||
|
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#toastc {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: .3em 0;
|
||||||
|
margin: -.3em 0 0 0;
|
||||||
|
line-height: 1.5em;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||||
|
}
|
||||||
|
#toast.vis {
|
||||||
|
right: 1.3em;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
#toast.vis #toastc {
|
||||||
|
left: -2em;
|
||||||
|
width: .4em;
|
||||||
|
padding: .3em .8em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#toast.inf {
|
||||||
|
background: #07a;
|
||||||
|
border-color: #0be;
|
||||||
|
}
|
||||||
|
#toast.inf #toastc {
|
||||||
|
background: #0be;
|
||||||
|
}
|
||||||
|
#toast.ok {
|
||||||
|
background: #4a0;
|
||||||
|
border-color: #8e4;
|
||||||
|
}
|
||||||
|
#toast.ok #toastc {
|
||||||
|
background: #8e4;
|
||||||
|
}
|
||||||
|
#toast.warn {
|
||||||
|
background: #970;
|
||||||
|
border-color: #fc0;
|
||||||
|
}
|
||||||
|
#toast.warn #toastc {
|
||||||
|
background: #fc0;
|
||||||
|
}
|
||||||
|
#toast.err {
|
||||||
|
background: #900;
|
||||||
|
border-color: #d06;
|
||||||
|
}
|
||||||
|
#toast.err #toastc {
|
||||||
|
background: #d06;
|
||||||
}
|
}
|
||||||
#tt.b {
|
#tt.b {
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
@@ -60,7 +136,6 @@ body {
|
|||||||
padding: .1em .3em;
|
padding: .1em .3em;
|
||||||
border-top: 1px solid #777;
|
border-top: 1px solid #777;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
font-family: monospace, monospace;
|
|
||||||
line-height: 1.7em;
|
line-height: 1.7em;
|
||||||
}
|
}
|
||||||
#tt em {
|
#tt em {
|
||||||
@@ -96,6 +171,10 @@ body {
|
|||||||
padding: .3em 0;
|
padding: .3em 0;
|
||||||
scroll-margin-top: 45vh;
|
scroll-margin-top: 45vh;
|
||||||
}
|
}
|
||||||
|
#files tr {
|
||||||
|
scroll-margin-top: 25vh;
|
||||||
|
scroll-margin-bottom: 20vh;
|
||||||
|
}
|
||||||
#files tbody div a {
|
#files tbody div a {
|
||||||
color: #f5a;
|
color: #f5a;
|
||||||
}
|
}
|
||||||
@@ -150,8 +229,7 @@ a, #files tbody div a:last-child {
|
|||||||
border-top: 1px solid #383838;
|
border-top: 1px solid #383838;
|
||||||
}
|
}
|
||||||
#files tbody td:nth-child(3) {
|
#files tbody td:nth-child(3) {
|
||||||
font-family: monospace;
|
font-family: monospace, monospace;
|
||||||
font-size: 1.3em;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -211,15 +289,31 @@ a, #files tbody div a:last-child {
|
|||||||
margin: .8em 0;
|
margin: .8em 0;
|
||||||
}
|
}
|
||||||
#srv_info {
|
#srv_info {
|
||||||
opacity: .5;
|
color: #a73;
|
||||||
font-size: .8em;
|
background: #333;
|
||||||
color: #fc5;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: .5em;
|
font-size: .8em;
|
||||||
|
top: .5em;
|
||||||
left: 2em;
|
left: 2em;
|
||||||
|
padding-right: .5em;
|
||||||
}
|
}
|
||||||
#srv_info span {
|
#srv_info span {
|
||||||
color: #fff;
|
color: #aaa;
|
||||||
|
}
|
||||||
|
#acc_info {
|
||||||
|
position: absolute;
|
||||||
|
font-size: .81em;
|
||||||
|
top: .5em;
|
||||||
|
right: 2em;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
#acc_info span {
|
||||||
|
color: #999;
|
||||||
|
margin-right: .6em;
|
||||||
|
}
|
||||||
|
#acc_info span.warn {
|
||||||
|
color: #f4c;
|
||||||
|
border-bottom: 1px solid rgba(255,68,204,0.6);
|
||||||
}
|
}
|
||||||
#files tbody a.play {
|
#files tbody a.play {
|
||||||
color: #e70;
|
color: #e70;
|
||||||
@@ -246,6 +340,7 @@ html.light #ggrid a.sel {
|
|||||||
border-color: #c37;
|
border-color: #c37;
|
||||||
}
|
}
|
||||||
#files tbody tr.sel:hover td,
|
#files tbody tr.sel:hover td,
|
||||||
|
#files tbody tr.sel:focus td,
|
||||||
#ggrid a.sel:hover,
|
#ggrid a.sel:hover,
|
||||||
html.light #ggrid a.sel:hover {
|
html.light #ggrid a.sel:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -280,6 +375,21 @@ html.light #ggrid a.sel {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 0 1px #fff;
|
text-shadow: 0 0 1px #fff;
|
||||||
}
|
}
|
||||||
|
#files tr:focus {
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#files tr:focus td {
|
||||||
|
background: #111;
|
||||||
|
border-color: #fc0 #111 #fc0 #111;
|
||||||
|
box-shadow: 0 .2em 0 #fc0, 0 -.2em 0 #fc0;
|
||||||
|
}
|
||||||
|
#files tr:focus td:first-child {
|
||||||
|
box-shadow: -.2em .2em 0 #fc0, -.2em -.2em 0 #fc0;
|
||||||
|
}
|
||||||
|
#files tr:focus+tr td {
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
}
|
||||||
#blocked {
|
#blocked {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -367,7 +477,6 @@ html.light #ggrid a.sel {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
top: -1.2em;
|
top: -1.2em;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 2.5em;
|
|
||||||
height: 1em;
|
height: 1em;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
@@ -376,7 +485,7 @@ html.light #ggrid a.sel {
|
|||||||
background: #3c3c3c;
|
background: #3c3c3c;
|
||||||
box-shadow: 0 0 .5em #222;
|
box-shadow: 0 0 .5em #222;
|
||||||
border-radius: .3em 0 0 0;
|
border-radius: .3em 0 0 0;
|
||||||
padding: .2em 0 0 .07em;
|
padding: .2em .2em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
#wzip, #wnp {
|
#wzip, #wnp {
|
||||||
@@ -398,12 +507,6 @@ html.light #ggrid a.sel {
|
|||||||
#wtoggle * {
|
#wtoggle * {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
#wtoggle.np {
|
|
||||||
width: 6.63em;
|
|
||||||
}
|
|
||||||
#wtoggle.sel {
|
|
||||||
width: 7.57em;
|
|
||||||
}
|
|
||||||
#wtoggle.sel #wzip,
|
#wtoggle.sel #wzip,
|
||||||
#wtoggle.np #wnp {
|
#wtoggle.np #wnp {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -411,15 +514,42 @@ html.light #ggrid a.sel {
|
|||||||
#wtoggle.sel.np #wnp {
|
#wtoggle.sel.np #wnp {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#wfm a,
|
||||||
#wzip a {
|
#wzip a {
|
||||||
font-size: .4em;
|
font-size: .5em;
|
||||||
padding: 0 .3em;
|
padding: 0 .3em;
|
||||||
margin: -.3em .2em;
|
margin: -.3em .2em;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
#wzip a+a {
|
#wfm span {
|
||||||
margin-left: .8em;
|
font-size: .6em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#wfm a:not(.en) {
|
||||||
|
opacity: .3;
|
||||||
|
color: #f6c;
|
||||||
|
}
|
||||||
|
html.light #wfm a:not(.en) {
|
||||||
|
color: #c4a;
|
||||||
|
}
|
||||||
|
#files tbody tr.c1 td {
|
||||||
|
animation: fcut1 .5s ease-out;
|
||||||
|
}
|
||||||
|
#files tbody tr.c2 td {
|
||||||
|
animation: fcut2 .5s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes fcut1 {
|
||||||
|
0% {opacity:0}
|
||||||
|
100% {opacity:1}
|
||||||
|
}
|
||||||
|
@keyframes fcut2 {
|
||||||
|
0% {opacity:0}
|
||||||
|
100% {opacity:1}
|
||||||
|
}
|
||||||
|
#wzip a {
|
||||||
|
font-size: .4em;
|
||||||
|
margin: -.3em .3em;
|
||||||
}
|
}
|
||||||
#wtoggle.sel #wzip #selzip {
|
#wtoggle.sel #wzip #selzip {
|
||||||
top: -.6em;
|
top: -.6em;
|
||||||
@@ -962,6 +1092,9 @@ html.light {
|
|||||||
html.light #tt {
|
html.light #tt {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-color: #888 #000 #777 #000;
|
border-color: #888 #000 #777 #000;
|
||||||
|
}
|
||||||
|
html.light #tt,
|
||||||
|
html.light #toast {
|
||||||
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
html.light #tt code {
|
html.light #tt code {
|
||||||
@@ -1001,10 +1134,14 @@ html.light .tgl.btn.on {
|
|||||||
}
|
}
|
||||||
html.light #srv_info {
|
html.light #srv_info {
|
||||||
color: #c83;
|
color: #c83;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
html.light #srv_info,
|
||||||
|
html.light #acc_info {
|
||||||
text-shadow: 1px 1px 0 #fff;
|
text-shadow: 1px 1px 0 #fff;
|
||||||
}
|
}
|
||||||
html.light #srv_info span {
|
html.light #srv_info span {
|
||||||
color: #000;
|
color: #777;
|
||||||
}
|
}
|
||||||
html.light #treeul a+a {
|
html.light #treeul a+a {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
@@ -1051,6 +1188,17 @@ html.light #files td {
|
|||||||
html.light #files tbody tr:last-child td {
|
html.light #files tbody tr:last-child td {
|
||||||
border-bottom: .2em solid #ccc;
|
border-bottom: .2em solid #ccc;
|
||||||
}
|
}
|
||||||
|
html.light #files tr:focus td {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #c37;
|
||||||
|
box-shadow: 0 .2em 0 #e80 , 0 -.2em 0 #e80;
|
||||||
|
}
|
||||||
|
html.light #files tr:focus td:first-child {
|
||||||
|
box-shadow: -.2em .2em 0 #e80, -.2em -.2em 0 #e80;
|
||||||
|
}
|
||||||
|
html.light #files tr.sel td {
|
||||||
|
background: #925;
|
||||||
|
}
|
||||||
html.light #files td:nth-child(2n) {
|
html.light #files td:nth-child(2n) {
|
||||||
color: #d38;
|
color: #d38;
|
||||||
}
|
}
|
||||||
@@ -1104,7 +1252,8 @@ html.light #wnp {
|
|||||||
html.light #barbuf {
|
html.light #barbuf {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
html.light #files tr.sel:hover td {
|
html.light #files tr.sel:hover td,
|
||||||
|
html.light #files tr.sel:focus td {
|
||||||
background: #c37;
|
background: #c37;
|
||||||
}
|
}
|
||||||
html.light #files tr.sel td {
|
html.light #files tr.sel td {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<div id="op_cfg" class="opview opbox opwide"></div>
|
<div id="op_cfg" class="opview opbox opwide"></div>
|
||||||
|
|
||||||
<h1 id="path">
|
<h1 id="path">
|
||||||
<a href="#" id="entree" tt="show directory tree$NHotkey: B">🌲</a>
|
<a href="#" id="entree" tt="show navpane (directory tree sidebar)$NHotkey: B">🌲</a>
|
||||||
{%- for n in vpnodes %}
|
{%- for n in vpnodes %}
|
||||||
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
@@ -121,10 +121,13 @@
|
|||||||
<div id="widget"></div>
|
<div id="widget"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var perms = {{ perms }},
|
var acct = "{{ acct }}",
|
||||||
|
perms = {{ perms }},
|
||||||
tag_order_cfg = {{ tag_order }},
|
tag_order_cfg = {{ tag_order }},
|
||||||
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
||||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||||
|
have_mv = {{ have_mv|tojson }},
|
||||||
|
have_del = {{ have_del|tojson }},
|
||||||
have_zip = {{ have_zip|tojson }};
|
have_zip = {{ have_zip|tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
|
|||||||
@@ -29,15 +29,20 @@ ebi('ops').innerHTML = (
|
|||||||
// media player
|
// media player
|
||||||
ebi('widget').innerHTML = (
|
ebi('widget').innerHTML = (
|
||||||
'<div id="wtoggle">' +
|
'<div id="wtoggle">' +
|
||||||
'<span id="wzip"><a' +
|
'<span id="wfm"><a' +
|
||||||
' href="#" id="selall" tt="select all files">sel.<br />all</a><a' +
|
' href="#" id="fren" tt="rename selected item$NHotkey: F2">✎<span>name</span></a><a' +
|
||||||
|
' href="#" id="fdel" tt="delete selected items$NHotkey: ctrl-K">⌫<span>delete</span></a><a' +
|
||||||
|
' href="#" id="fcut" tt="cut selected items <small>(then paste somewhere else)</small>$NHotkey: ctrl-X">✂<span>cut</span></a><a' +
|
||||||
|
' href="#" id="fpst" tt="paste a previously cut/copied selection$NHotkey: ctrl-V">📋<span>paste</span></a>' +
|
||||||
|
'</span><span id="wzip"><a' +
|
||||||
|
' href="#" id="selall" tt="select all files$NHotkey: ctrl-A (when file focused)">sel.<br />all</a><a' +
|
||||||
' href="#" id="selinv" tt="invert selection">sel.<br />inv.</a><a' +
|
' href="#" id="selinv" tt="invert selection">sel.<br />inv.</a><a' +
|
||||||
' href="#" id="selzip" tt="download selection as archive">zip</a>' +
|
' href="#" id="selzip" tt="download selection as archive">zip</a>' +
|
||||||
'</span><span id="wnp"><a' +
|
'</span><span id="wnp"><a' +
|
||||||
' href="#" id="npirc" tt="copy irc-formatted track info">📋irc</a><a' +
|
' href="#" id="npirc" tt="copy irc-formatted track info">📋irc</a><a' +
|
||||||
' href="#" id="nptxt" tt="copy plaintext track info">📋txt</a>' +
|
' href="#" id="nptxt" tt="copy plaintext track info">📋txt</a>' +
|
||||||
'</span><a' +
|
'</span><a' +
|
||||||
' href="#" id="wtgrid">田</a><a' +
|
' href="#" id="wtgrid" tt="toggle grid/list view">田</a><a' +
|
||||||
' href="#" id="wtico">♫</a>' +
|
' href="#" id="wtico">♫</a>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div id="widgeti">' +
|
'<div id="widgeti">' +
|
||||||
@@ -62,7 +67,7 @@ ebi('op_up2k').innerHTML = (
|
|||||||
' </td>\n' +
|
' </td>\n' +
|
||||||
' <td rowspan="2">\n' +
|
' <td rowspan="2">\n' +
|
||||||
' <input type="checkbox" id="ask_up" />\n' +
|
' <input type="checkbox" id="ask_up" />\n' +
|
||||||
' <label for="ask_up" tt="ask for confirmation befofre upload starts">💭</label>\n' +
|
' <label for="ask_up" tt="ask for confirmation before upload starts">💭</label>\n' +
|
||||||
' </td>\n' +
|
' </td>\n' +
|
||||||
' <td rowspan="2">\n' +
|
' <td rowspan="2">\n' +
|
||||||
' <input type="checkbox" id="flag_en" />\n' +
|
' <input type="checkbox" id="flag_en" />\n' +
|
||||||
@@ -146,7 +151,7 @@ ebi('op_cfg').innerHTML = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// tree sidebar
|
// navpane
|
||||||
ebi('tree').innerHTML = (
|
ebi('tree').innerHTML = (
|
||||||
'<div id="treeh">\n' +
|
'<div id="treeh">\n' +
|
||||||
' <a href="#" id="detree" tt="show breadcrumbs$NHotkey: B">🍞...</a>\n' +
|
' <a href="#" id="detree" tt="show breadcrumbs$NHotkey: B">🍞...</a>\n' +
|
||||||
@@ -177,8 +182,10 @@ function opclick(e) {
|
|||||||
goto(dest);
|
goto(dest);
|
||||||
|
|
||||||
var input = QS('.opview.act input:not([type="hidden"])')
|
var input = QS('.opview.act input:not([type="hidden"])')
|
||||||
if (input && !is_touch)
|
if (input && !is_touch) {
|
||||||
|
tt.skip = true;
|
||||||
input.focus();
|
input.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -278,7 +285,7 @@ var mpl = (function () {
|
|||||||
r.os_ctl = !r.os_ctl && have_mctl;
|
r.os_ctl = !r.os_ctl && have_mctl;
|
||||||
bcfg_set('au_os_ctl', r.os_ctl);
|
bcfg_set('au_os_ctl', r.os_ctl);
|
||||||
if (!have_mctl)
|
if (!have_mctl)
|
||||||
alert('need firefox 82+ or chrome 73+');
|
toast.err(5, 'need firefox 82+ or chrome 73+');
|
||||||
};
|
};
|
||||||
|
|
||||||
ebi('au_osd_cv').onclick = function (e) {
|
ebi('au_osd_cv').onclick = function (e) {
|
||||||
@@ -1144,7 +1151,7 @@ var audio_eq = (function () {
|
|||||||
v = parseFloat(vs);
|
v = parseFloat(vs);
|
||||||
|
|
||||||
if (isNaN(v) || v + '' != vs)
|
if (isNaN(v) || v + '' != vs)
|
||||||
throw 42;
|
throw new Error('inval band');
|
||||||
|
|
||||||
if (isNaN(band))
|
if (isNaN(band))
|
||||||
r.amp = Math.round((v + step * 0.2) * 100) / 100;
|
r.amp = Math.round((v + step * 0.2) * 100) / 100;
|
||||||
@@ -1346,7 +1353,7 @@ function play(tid, is_ev, seek, call_depth) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
alert('playback failed: ' + ex);
|
toast.err(0, 'playback failed: ' + ex);
|
||||||
}
|
}
|
||||||
setclass(oid, 'play');
|
setclass(oid, 'play');
|
||||||
setTimeout(next_song, 500);
|
setTimeout(next_song, 500);
|
||||||
@@ -1450,6 +1457,249 @@ function play_linked() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var d = mknod('div');
|
||||||
|
d.setAttribute('id', 'acc_info');
|
||||||
|
document.body.insertBefore(d, ebi('ops'));
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var fileman = (function () {
|
||||||
|
var bren = ebi('fren'),
|
||||||
|
bdel = ebi('fdel'),
|
||||||
|
bcut = ebi('fcut'),
|
||||||
|
bpst = ebi('fpst'),
|
||||||
|
r = {};
|
||||||
|
|
||||||
|
r.clip = null;
|
||||||
|
r.bus = new BroadcastChannel("fileman_bus");
|
||||||
|
|
||||||
|
r.render = function () {
|
||||||
|
if (r.clip === null)
|
||||||
|
r.clip = jread('fman_clip', []);
|
||||||
|
|
||||||
|
var sel = msel.getsel();
|
||||||
|
clmod(bren, 'en', sel.length == 1);
|
||||||
|
clmod(bdel, 'en', sel.length);
|
||||||
|
clmod(bcut, 'en', sel.length);
|
||||||
|
clmod(bpst, 'en', r.clip && r.clip.length);
|
||||||
|
bren.style.display = have_mv && has(perms, 'write') && has(perms, 'move') ? '' : 'none';
|
||||||
|
bdel.style.display = have_del && has(perms, 'delete') ? '' : 'none';
|
||||||
|
bcut.style.display = have_mv && has(perms, 'move') ? '' : 'none';
|
||||||
|
bpst.style.display = have_mv && has(perms, 'write') ? '' : 'none';
|
||||||
|
bpst.setAttribute('tt', 'paste ' + r.clip.length + ' items$NHotkey: ctrl-V');
|
||||||
|
ebi('wfm').style.display = QS('#wfm a.en:not([display])') ? '' : 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
r.rename = function (e) {
|
||||||
|
ev(e);
|
||||||
|
if (bren.style.display)
|
||||||
|
return toast.err(3, 'cannot rename:\nyou do not have “move” permission in this folder');
|
||||||
|
|
||||||
|
var sel = msel.getsel();
|
||||||
|
if (sel.length !== 1)
|
||||||
|
return toast.err(3, 'select exactly 1 item to rename');
|
||||||
|
|
||||||
|
var src = sel[0].vp;
|
||||||
|
if (src.endsWith('/'))
|
||||||
|
src = src.slice(0, -1);
|
||||||
|
|
||||||
|
var vsp = vsplit(src),
|
||||||
|
base = vsp[0],
|
||||||
|
ofn = vsp[1];
|
||||||
|
|
||||||
|
var fn = prompt('new filename:', ofn);
|
||||||
|
if (!fn || fn == ofn)
|
||||||
|
return toast.warn(1, 'rename aborted');
|
||||||
|
|
||||||
|
var dst = base + fn;
|
||||||
|
|
||||||
|
function rename_cb() {
|
||||||
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.status !== 200) {
|
||||||
|
var msg = this.responseText;
|
||||||
|
toast.err(9, 'rename failed:\n' + msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.ok(2, 'rename OK');
|
||||||
|
treectl.goto(get_evpath());
|
||||||
|
}
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', src + '?move=' + dst, true);
|
||||||
|
xhr.onreadystatechange = rename_cb;
|
||||||
|
xhr.send();
|
||||||
|
};
|
||||||
|
|
||||||
|
r.delete = function (e) {
|
||||||
|
ev(e);
|
||||||
|
if (bdel.style.display)
|
||||||
|
return toast.err(3, 'cannot delete:\nyou do not have “delete” permission in this folder');
|
||||||
|
|
||||||
|
var sel = msel.getsel(),
|
||||||
|
vps = [];
|
||||||
|
|
||||||
|
for (var a = 0; a < sel.length; a++)
|
||||||
|
vps.push(sel[a].vp);
|
||||||
|
|
||||||
|
if (!sel.length)
|
||||||
|
return toast.err(3, 'select at least 1 item to delete');
|
||||||
|
|
||||||
|
if (!confirm('===== DANGER =====\nDELETE these ' + vps.length + ' items?\n\n' + vps.join('\n')))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!confirm('Last chance! Delete?'))
|
||||||
|
return;
|
||||||
|
|
||||||
|
function deleter() {
|
||||||
|
var xhr = new XMLHttpRequest(),
|
||||||
|
vp = vps.shift();
|
||||||
|
|
||||||
|
if (!vp) {
|
||||||
|
toast.ok(2, 'delete OK');
|
||||||
|
treectl.goto(get_evpath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.inf(0, 'deleting ' + (vps.length + 1) + ' items\n\n' + vp);
|
||||||
|
|
||||||
|
xhr.open('GET', vp + '?delete', true);
|
||||||
|
xhr.onreadystatechange = delete_cb;
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
function delete_cb() {
|
||||||
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.status !== 200) {
|
||||||
|
var msg = this.responseText;
|
||||||
|
toast.err(9, 'delete failed:\n' + msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleter();
|
||||||
|
}
|
||||||
|
deleter();
|
||||||
|
};
|
||||||
|
|
||||||
|
r.cut = function (e) {
|
||||||
|
ev(e);
|
||||||
|
if (bcut.style.display)
|
||||||
|
return toast.err(3, 'cannot cut:\nyou do not have “move” permission in this folder');
|
||||||
|
|
||||||
|
var sel = msel.getsel(),
|
||||||
|
vps = [];
|
||||||
|
|
||||||
|
if (!sel.length)
|
||||||
|
return toast.err(3, 'select at least 1 item to cut');
|
||||||
|
|
||||||
|
for (var a = 0; a < sel.length; a++) {
|
||||||
|
vps.push(sel[a].vp);
|
||||||
|
var cl = ebi(sel[a].id).closest('tr').classList,
|
||||||
|
inv = cl.contains('c1');
|
||||||
|
|
||||||
|
cl.remove(inv ? 'c1' : 'c2');
|
||||||
|
cl.add(inv ? 'c2' : 'c1');
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.inf(1, 'cut ' + sel.length + ' items');
|
||||||
|
jwrite('fman_clip', vps);
|
||||||
|
r.tx(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
r.paste = function (e) {
|
||||||
|
ev(e);
|
||||||
|
if (bpst.style.display)
|
||||||
|
return toast.err(3, 'cannot paste:\nyou do not have “write” permission in this folder');
|
||||||
|
|
||||||
|
if (!r.clip.length)
|
||||||
|
return toast.err(5, 'first cut some files/folders to paste\n\nnote: you can cut/paste across different browser tabs');
|
||||||
|
|
||||||
|
var req = [],
|
||||||
|
exists = [],
|
||||||
|
indir = [],
|
||||||
|
srcdir = vsplit(r.clip[0])[0],
|
||||||
|
links = QSA('#files tbody td:nth-child(2) a');
|
||||||
|
|
||||||
|
for (var a = 0, aa = links.length; a < aa; a++)
|
||||||
|
indir.push(links[a].getAttribute('name'));
|
||||||
|
|
||||||
|
for (var a = 0; a < r.clip.length; a++) {
|
||||||
|
var found = false;
|
||||||
|
for (var b = 0; b < indir.length; b++) {
|
||||||
|
if (r.clip[a].endsWith('/' + indir[b])) {
|
||||||
|
exists.push(r.clip[a]);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found)
|
||||||
|
req.push(r.clip[a]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists.length)
|
||||||
|
alert('these ' + exists.length + ' items cannot be pasted here (names already exist):\n\n' + exists.join('\n'));
|
||||||
|
|
||||||
|
if (!req.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!confirm('paste these ' + req.length + ' items here?\n\n' + req.join('\n')))
|
||||||
|
return;
|
||||||
|
|
||||||
|
function paster() {
|
||||||
|
var xhr = new XMLHttpRequest(),
|
||||||
|
vp = req.shift();
|
||||||
|
|
||||||
|
if (!vp) {
|
||||||
|
toast.ok(2, 'paste OK');
|
||||||
|
treectl.goto(get_evpath());
|
||||||
|
r.tx(srcdir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.inf(0, 'pasting ' + (req.length + 1) + ' items\n\n' + vp);
|
||||||
|
|
||||||
|
var dst = get_evpath() + vp.split('/').slice(-1)[0];
|
||||||
|
|
||||||
|
xhr.open('GET', vp + '?move=' + dst, true);
|
||||||
|
xhr.onreadystatechange = paste_cb;
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
function paste_cb() {
|
||||||
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.status !== 200) {
|
||||||
|
var msg = this.responseText;
|
||||||
|
toast.err(9, 'paste failed:\n' + msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
paster();
|
||||||
|
}
|
||||||
|
paster();
|
||||||
|
|
||||||
|
jwrite('fman_clip', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
r.bus.onmessage = function (e) {
|
||||||
|
r.clip = null;
|
||||||
|
r.render();
|
||||||
|
var me = get_evpath();
|
||||||
|
if (e && e.data == me)
|
||||||
|
treectl.goto(e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
r.tx = function (msg) {
|
||||||
|
r.bus.postMessage(msg);
|
||||||
|
r.bus.onmessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
bren.onclick = r.rename;
|
||||||
|
bdel.onclick = r.delete;
|
||||||
|
bcut.onclick = r.cut;
|
||||||
|
bpst.onclick = r.paste;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
var thegrid = (function () {
|
var thegrid = (function () {
|
||||||
var lfiles = ebi('files'),
|
var lfiles = ebi('files'),
|
||||||
gfiles = mknod('div');
|
gfiles = mknod('div');
|
||||||
@@ -1488,9 +1738,6 @@ var thegrid = (function () {
|
|||||||
|
|
||||||
ebi('griden').onclick = ebi('wtgrid').onclick = function (e) {
|
ebi('griden').onclick = ebi('wtgrid').onclick = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
if (!this.closest)
|
|
||||||
return;
|
|
||||||
|
|
||||||
r.en = !r.en;
|
r.en = !r.en;
|
||||||
bcfg_set('griden', r.en);
|
bcfg_set('griden', r.en);
|
||||||
if (r.en) {
|
if (r.en) {
|
||||||
@@ -1711,6 +1958,26 @@ var thegrid = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function tree_scrollto() {
|
||||||
|
var act = QS('#treeul a.hl'),
|
||||||
|
ul = act ? act.offsetParent : null;
|
||||||
|
|
||||||
|
if (!ul)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ctr = ebi('tree'),
|
||||||
|
em = parseFloat(getComputedStyle(act).fontSize),
|
||||||
|
top = act.offsetTop + ul.offsetTop,
|
||||||
|
min = top - 11 * em,
|
||||||
|
max = top - (ctr.offsetHeight - 10 * em);
|
||||||
|
|
||||||
|
if (ctr.scrollTop > min)
|
||||||
|
ctr.scrollTop = Math.floor(min);
|
||||||
|
else if (ctr.scrollTop < max)
|
||||||
|
ctr.scrollTop = Math.floor(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function tree_neigh(n) {
|
function tree_neigh(n) {
|
||||||
var links = QSA('#treeul li>a+a');
|
var links = QSA('#treeul li>a+a');
|
||||||
if (!links.length) {
|
if (!links.length) {
|
||||||
@@ -1736,6 +2003,7 @@ function tree_neigh(n) {
|
|||||||
if (act >= links.length)
|
if (act >= links.length)
|
||||||
act = 0;
|
act = 0;
|
||||||
|
|
||||||
|
treectl.dir_cb = tree_scrollto;
|
||||||
links[act].click();
|
links[act].click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,10 +2023,11 @@ function tree_up() {
|
|||||||
|
|
||||||
|
|
||||||
document.onkeydown = function (e) {
|
document.onkeydown = function (e) {
|
||||||
if (!document.activeElement || document.activeElement != document.body && document.activeElement.nodeName.toLowerCase() != 'a')
|
var ae = document.activeElement, aet = '';
|
||||||
return;
|
if (ae && ae != document.body)
|
||||||
|
aet = ae.nodeName.toLowerCase();
|
||||||
|
|
||||||
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
if (e.altKey || e.isComposing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (QS('#bbox-overlay.visible'))
|
if (QS('#bbox-overlay.visible'))
|
||||||
@@ -1766,6 +2035,55 @@ document.onkeydown = function (e) {
|
|||||||
|
|
||||||
var k = e.code + '', pos = -1, n;
|
var k = e.code + '', pos = -1, n;
|
||||||
|
|
||||||
|
if (aet == 'tr' && ae.closest('#files')) {
|
||||||
|
var d = '';
|
||||||
|
if (k == 'ArrowUp') d = 'previous';
|
||||||
|
if (k == 'ArrowDown') d = 'next';
|
||||||
|
if (d) {
|
||||||
|
var el = ae[d + 'ElementSibling'];
|
||||||
|
if (el) {
|
||||||
|
el.focus();
|
||||||
|
if (ctrl(e))
|
||||||
|
document.documentElement.scrollTop += (d == 'next' ? 1 : -1) * el.offsetHeight;
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
clmod(el, 'sel', 't');
|
||||||
|
msel.selui();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (k == 'Space') {
|
||||||
|
clmod(ae, 'sel', 't');
|
||||||
|
msel.selui();
|
||||||
|
return ev(e);
|
||||||
|
}
|
||||||
|
if (k == 'KeyA' && ctrl(e)) {
|
||||||
|
var sel = msel.getsel(),
|
||||||
|
all = msel.getall();
|
||||||
|
|
||||||
|
msel.evsel(e, sel.length < all.length);
|
||||||
|
return ev(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aet && aet != 'a' && aet != 'tr')
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ctrl(e)) {
|
||||||
|
if (k == 'KeyX')
|
||||||
|
return fileman.cut();
|
||||||
|
|
||||||
|
if (k == 'KeyV')
|
||||||
|
return fileman.paste();
|
||||||
|
|
||||||
|
if (k == 'KeyK')
|
||||||
|
return fileman.delete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.shiftKey && k != 'KeyA' && k != 'KeyD')
|
if (e.shiftKey && k != 'KeyA' && k != 'KeyD')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1804,6 +2122,9 @@ document.onkeydown = function (e) {
|
|||||||
if (k == 'KeyT')
|
if (k == 'KeyT')
|
||||||
return ebi('thumbs').click();
|
return ebi('thumbs').click();
|
||||||
|
|
||||||
|
if (k == 'F2')
|
||||||
|
return fileman.rename();
|
||||||
|
|
||||||
if (!treectl.hidden && (!e.shiftKey || !thegrid.en)) {
|
if (!treectl.hidden && (!e.shiftKey || !thegrid.en)) {
|
||||||
if (k == 'KeyA')
|
if (k == 'KeyA')
|
||||||
return QS('#twig').click();
|
return QS('#twig').click();
|
||||||
@@ -1940,7 +2261,7 @@ document.onkeydown = function (e) {
|
|||||||
for (var b = 1; b < sconf[a].length; b++) {
|
for (var b = 1; b < sconf[a].length; b++) {
|
||||||
var k = sconf[a][b][0],
|
var k = sconf[a][b][0],
|
||||||
chk = 'srch_' + k + 'c',
|
chk = 'srch_' + k + 'c',
|
||||||
tvs = ebi('srch_' + k + 'v').value.split(/ /g);
|
tvs = ebi('srch_' + k + 'v').value.split(/ +/g);
|
||||||
|
|
||||||
if (!ebi(chk).checked)
|
if (!ebi(chk).checked)
|
||||||
continue;
|
continue;
|
||||||
@@ -1953,7 +2274,7 @@ document.onkeydown = function (e) {
|
|||||||
q += ' and ';
|
q += ' and ';
|
||||||
|
|
||||||
if (k == 'adv') {
|
if (k == 'adv') {
|
||||||
q += tv.replace(/ /g, " and ").replace(/([=!><]=?)/, " $1 ");
|
q += tv.replace(/ +/g, " and ").replace(/([=!><]=?)/, " $1 ");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2088,7 +2409,6 @@ document.onkeydown = function (e) {
|
|||||||
ebi('files').innerHTML = orig_html;
|
ebi('files').innerHTML = orig_html;
|
||||||
ebi('files').removeAttribute('q_raw');
|
ebi('files').removeAttribute('q_raw');
|
||||||
orig_html = null;
|
orig_html = null;
|
||||||
msel.render();
|
|
||||||
reload_browser();
|
reload_browser();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -2234,7 +2554,7 @@ var treectl = (function () {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200) {
|
||||||
alert("http " + this.status + ": " + this.responseText);
|
toast.err(0, "recvtree, http " + this.status + ": " + this.responseText);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2284,7 +2604,12 @@ var treectl = (function () {
|
|||||||
var fun = treectl.dir_cb;
|
var fun = treectl.dir_cb;
|
||||||
if (fun) {
|
if (fun) {
|
||||||
treectl.dir_cb = null;
|
treectl.dir_cb = null;
|
||||||
fun();
|
try {
|
||||||
|
fun();
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.log("dir_cb failed", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2351,7 +2676,7 @@ var treectl = (function () {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200) {
|
||||||
alert("http " + this.status + ": " + this.responseText);
|
toast.err(0, "recvls, http " + this.status + ": " + this.responseText);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2406,6 +2731,7 @@ var treectl = (function () {
|
|||||||
if (this.hpush)
|
if (this.hpush)
|
||||||
hist_push(this.top);
|
hist_push(this.top);
|
||||||
|
|
||||||
|
acct = res.acct;
|
||||||
apply_perms(res.perms);
|
apply_perms(res.perms);
|
||||||
despin('#files');
|
despin('#files');
|
||||||
despin('#gfiles');
|
despin('#gfiles');
|
||||||
@@ -2417,7 +2743,6 @@ var treectl = (function () {
|
|||||||
|
|
||||||
filecols.set_style();
|
filecols.set_style();
|
||||||
mukey.render();
|
mukey.render();
|
||||||
msel.render();
|
|
||||||
reload_tree();
|
reload_tree();
|
||||||
reload_browser();
|
reload_browser();
|
||||||
|
|
||||||
@@ -2522,6 +2847,23 @@ function despin(sel) {
|
|||||||
function apply_perms(newperms) {
|
function apply_perms(newperms) {
|
||||||
perms = newperms || [];
|
perms = newperms || [];
|
||||||
|
|
||||||
|
var axs = [],
|
||||||
|
aclass = '>',
|
||||||
|
chk = ['read', 'write', 'rename', 'delete'];
|
||||||
|
|
||||||
|
for (var a = 0; a < chk.length; a++)
|
||||||
|
if (has(perms, chk[a]))
|
||||||
|
axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1));
|
||||||
|
|
||||||
|
axs = axs.join('-');
|
||||||
|
if (perms.length == 1) {
|
||||||
|
aclass = ' class="warn">';
|
||||||
|
axs += '-Only';
|
||||||
|
}
|
||||||
|
|
||||||
|
ebi('acc_info').innerHTML = '<span' + aclass + axs + ' access</span>' + (acct != '*' ?
|
||||||
|
'<a href="/?pw=x">Logout ' + acct + '</a>' : '<a href="/?h">Login</a>');
|
||||||
|
|
||||||
var o = QSA('#ops>a[data-perm], #u2footfoot');
|
var o = QSA('#ops>a[data-perm], #u2footfoot');
|
||||||
for (var a = 0; a < o.length; a++) {
|
for (var a = 0; a < o.length; a++) {
|
||||||
var display = '';
|
var display = '';
|
||||||
@@ -2545,12 +2887,10 @@ function apply_perms(newperms) {
|
|||||||
de = document.documentElement,
|
de = document.documentElement,
|
||||||
tds = QSA('#u2conf td');
|
tds = QSA('#u2conf td');
|
||||||
|
|
||||||
/* good idea maybe
|
|
||||||
clmod(de, "read", have_read);
|
clmod(de, "read", have_read);
|
||||||
clmod(de, "write", have_write);
|
clmod(de, "write", have_write);
|
||||||
clmod(de, "nread", !have_read);
|
clmod(de, "nread", !have_read);
|
||||||
clmod(de, "nwrite", !have_write);
|
clmod(de, "nwrite", !have_write);
|
||||||
*/
|
|
||||||
|
|
||||||
for (var a = 0; a < tds.length; a++) {
|
for (var a = 0; a < tds.length; a++) {
|
||||||
tds[a].style.display =
|
tds[a].style.display =
|
||||||
@@ -2576,7 +2916,7 @@ function find_file_col(txt) {
|
|||||||
for (var a = 0; a < tds.length; a++) {
|
for (var a = 0; a < tds.length; a++) {
|
||||||
var spans = tds[a].getElementsByTagName('span');
|
var spans = tds[a].getElementsByTagName('span');
|
||||||
if (spans.length && spans[0].textContent == txt) {
|
if (spans.length && spans[0].textContent == txt) {
|
||||||
min = tds[a].getAttribute('class').indexOf('min') !== -1;
|
min = (tds[a].getAttribute('class') || '').indexOf('min') !== -1;
|
||||||
i = a;
|
i = a;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2706,6 +3046,10 @@ var filecols = (function () {
|
|||||||
for (var b = 0, bb = tds.length; b < bb; b++)
|
for (var b = 0, bb = tds.length; b < bb; b++)
|
||||||
tds[b].setAttribute('class', cls);
|
tds[b].setAttribute('class', cls);
|
||||||
}
|
}
|
||||||
|
if (window['tt']) {
|
||||||
|
tt.att(ebi('hcols'));
|
||||||
|
tt.att(QS('#files thead'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
set_style();
|
set_style();
|
||||||
|
|
||||||
@@ -2928,7 +3272,7 @@ var arcfmt = (function () {
|
|||||||
|
|
||||||
var ofs = href.lastIndexOf('?');
|
var ofs = href.lastIndexOf('?');
|
||||||
if (ofs < 0)
|
if (ofs < 0)
|
||||||
throw 'missing arg in url';
|
throw new Error('missing arg in url');
|
||||||
|
|
||||||
o.setAttribute("href", href.slice(0, ofs + 1) + arg);
|
o.setAttribute("href", href.slice(0, ofs + 1) + arg);
|
||||||
o.textContent = fmt.split('_')[0];
|
o.textContent = fmt.split('_')[0];
|
||||||
@@ -2965,41 +3309,73 @@ var arcfmt = (function () {
|
|||||||
|
|
||||||
|
|
||||||
var msel = (function () {
|
var msel = (function () {
|
||||||
function getsel() {
|
var r = {};
|
||||||
var names = [],
|
r.sel = null;
|
||||||
links = QSA('#files tbody tr.sel td:nth-child(2) a');
|
r.all = null;
|
||||||
|
|
||||||
for (var a = 0, aa = links.length; a < aa; a++)
|
r.load = function () {
|
||||||
names.push(links[a].getAttribute('href').replace(/\/$/, "").split('/').slice(-1));
|
if (r.sel)
|
||||||
|
return;
|
||||||
|
|
||||||
return names;
|
r.sel = [];
|
||||||
}
|
r.all = [];
|
||||||
function selui() {
|
var links = QSA('#files tbody td:nth-child(2) a:last-child'),
|
||||||
clmod(ebi('wtoggle'), 'sel', getsel().length);
|
vbase = get_evpath();
|
||||||
|
|
||||||
|
for (var a = 0, aa = links.length; a < aa; a++) {
|
||||||
|
var href = links[a].getAttribute('href').replace(/\/$/, ""),
|
||||||
|
item = {};
|
||||||
|
|
||||||
|
item.id = links[a].getAttribute('id');
|
||||||
|
item.sel = links[a].closest('tr').classList.contains('sel');
|
||||||
|
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
|
||||||
|
item.name = href.split('/').slice(-1);
|
||||||
|
|
||||||
|
r.all.push(item);
|
||||||
|
if (item.sel)
|
||||||
|
r.sel.push(item);
|
||||||
|
|
||||||
|
links[a].setAttribute('name', item.name);
|
||||||
|
links[a].closest('tr').setAttribute('tabindex', '0');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
r.getsel = function () {
|
||||||
|
r.load();
|
||||||
|
return r.sel;
|
||||||
|
};
|
||||||
|
r.getall = function () {
|
||||||
|
r.load();
|
||||||
|
return r.all;
|
||||||
|
};
|
||||||
|
r.selui = function () {
|
||||||
|
r.sel = r.all = null;
|
||||||
|
clmod(ebi('wtoggle'), 'sel', r.getsel().length);
|
||||||
thegrid.loadsel();
|
thegrid.loadsel();
|
||||||
|
fileman.render();
|
||||||
}
|
}
|
||||||
function seltgl(e) {
|
r.seltgl = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
var tr = this.parentNode;
|
var tr = this.parentNode;
|
||||||
clmod(tr, 'sel', 't');
|
clmod(tr, 'sel', 't');
|
||||||
selui();
|
r.selui();
|
||||||
}
|
}
|
||||||
function evsel(e, fun) {
|
r.evsel = function (e, fun) {
|
||||||
ev(e);
|
ev(e);
|
||||||
var trs = QSA('#files tbody tr');
|
var trs = QSA('#files tbody tr');
|
||||||
for (var a = 0, aa = trs.length; a < aa; a++)
|
for (var a = 0, aa = trs.length; a < aa; a++)
|
||||||
clmod(trs[a], 'sel', fun);
|
clmod(trs[a], 'sel', fun);
|
||||||
selui();
|
r.selui();
|
||||||
}
|
}
|
||||||
ebi('selall').onclick = function (e) {
|
ebi('selall').onclick = function (e) {
|
||||||
evsel(e, "add");
|
r.evsel(e, "add");
|
||||||
};
|
};
|
||||||
ebi('selinv').onclick = function (e) {
|
ebi('selinv').onclick = function (e) {
|
||||||
evsel(e, "t");
|
r.evsel(e, "t");
|
||||||
};
|
};
|
||||||
ebi('selzip').onclick = function (e) {
|
ebi('selzip').onclick = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
var names = getsel(),
|
var names = r.getsel(),
|
||||||
arg = ebi('selzip').getAttribute('fmt'),
|
arg = ebi('selzip').getAttribute('fmt'),
|
||||||
txt = names.join('\n'),
|
txt = names.join('\n'),
|
||||||
frm = mknod('form');
|
frm = mknod('form');
|
||||||
@@ -3022,16 +3398,17 @@ var msel = (function () {
|
|||||||
console.log(txt);
|
console.log(txt);
|
||||||
frm.submit();
|
frm.submit();
|
||||||
};
|
};
|
||||||
function render() {
|
r.render = function () {
|
||||||
var tds = QSA('#files tbody td+td+td');
|
var tds = QSA('#files tbody td+td+td');
|
||||||
for (var a = 0, aa = tds.length; a < aa; a++) {
|
for (var a = 0, aa = tds.length; a < aa; a++) {
|
||||||
tds[a].onclick = seltgl;
|
tds[a].onclick = r.seltgl;
|
||||||
}
|
}
|
||||||
|
r.selui();
|
||||||
arcfmt.render();
|
arcfmt.render();
|
||||||
|
fileman.render();
|
||||||
|
ebi('selzip').style.display = ebi('unsearch') ? 'none' : '';
|
||||||
}
|
}
|
||||||
return {
|
return r;
|
||||||
"render": render
|
|
||||||
};
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
@@ -3090,7 +3467,7 @@ function reload_browser(not_mp) {
|
|||||||
|
|
||||||
var oo = QSA('#files>tbody>tr>td:nth-child(3)');
|
var oo = QSA('#files>tbody>tr>td:nth-child(3)');
|
||||||
for (var a = 0, aa = oo.length; a < aa; a++) {
|
for (var a = 0, aa = oo.length; a < aa; a++) {
|
||||||
var sz = oo[a].textContent.replace(/ /g, ""),
|
var sz = oo[a].textContent.replace(/ +/g, ""),
|
||||||
hsz = sz.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
hsz = sz.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||||
|
|
||||||
oo[a].textContent = hsz;
|
oo[a].textContent = hsz;
|
||||||
@@ -3106,8 +3483,8 @@ function reload_browser(not_mp) {
|
|||||||
up2k.set_fsearch();
|
up2k.set_fsearch();
|
||||||
|
|
||||||
thegrid.setdirty();
|
thegrid.setdirty();
|
||||||
|
msel.render();
|
||||||
}
|
}
|
||||||
reload_browser(true);
|
reload_browser(true);
|
||||||
mukey.render();
|
mukey.render();
|
||||||
msel.render();
|
|
||||||
play_linked();
|
play_linked();
|
||||||
|
|||||||
@@ -8,20 +8,93 @@ html, body {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
#tt {
|
|
||||||
|
|
||||||
|
|
||||||
|
#tt, #toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
max-width: 34em;
|
max-width: 34em;
|
||||||
background: #222;
|
background: #222;
|
||||||
border: 0 solid #777;
|
border: 0 solid #777;
|
||||||
|
box-shadow: 0 .2em .5em #222;
|
||||||
|
border-radius: .4em;
|
||||||
|
z-index: 9001;
|
||||||
|
}
|
||||||
|
#tt {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
padding: 0 1.3em;
|
padding: 0 1.3em;
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: .1;
|
opacity: .1;
|
||||||
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||||
box-shadow: 0 .2em .5em #222;
|
}
|
||||||
border-radius: .4em;
|
#toast {
|
||||||
z-index: 9001;
|
top: 1.4em;
|
||||||
|
right: -1em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition:
|
||||||
|
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||||
|
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#toastc {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: .3em 0;
|
||||||
|
margin: -.3em 0 0 0;
|
||||||
|
line-height: 1.5em;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||||
|
}
|
||||||
|
#toast.vis {
|
||||||
|
right: 1.3em;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
#toast.vis #toastc {
|
||||||
|
left: -2em;
|
||||||
|
width: .4em;
|
||||||
|
padding: .3em .8em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#toast.inf {
|
||||||
|
background: #07a;
|
||||||
|
border-color: #0be;
|
||||||
|
}
|
||||||
|
#toast.inf #toastc {
|
||||||
|
background: #0be;
|
||||||
|
}
|
||||||
|
#toast.ok {
|
||||||
|
background: #4a0;
|
||||||
|
border-color: #8e4;
|
||||||
|
}
|
||||||
|
#toast.ok #toastc {
|
||||||
|
background: #8e4;
|
||||||
|
}
|
||||||
|
#toast.warn {
|
||||||
|
background: #970;
|
||||||
|
border-color: #fc0;
|
||||||
|
}
|
||||||
|
#toast.warn #toastc {
|
||||||
|
background: #fc0;
|
||||||
|
}
|
||||||
|
#toast.err {
|
||||||
|
background: #900;
|
||||||
|
border-color: #d06;
|
||||||
|
}
|
||||||
|
#toast.err #toastc {
|
||||||
|
background: #d06;
|
||||||
}
|
}
|
||||||
#tt.b {
|
#tt.b {
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
@@ -43,12 +116,29 @@ html, body {
|
|||||||
padding: .1em .3em;
|
padding: .1em .3em;
|
||||||
border-top: 1px solid #777;
|
border-top: 1px solid #777;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
font-family: monospace, monospace;
|
|
||||||
line-height: 1.7em;
|
line-height: 1.7em;
|
||||||
}
|
}
|
||||||
#tt em {
|
#tt em {
|
||||||
color: #f6a;
|
color: #f6a;
|
||||||
}
|
}
|
||||||
|
html.light #tt {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #888 #000 #777 #000;
|
||||||
|
}
|
||||||
|
html.light #tt,
|
||||||
|
html.light #toast {
|
||||||
|
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
html.light #tt code {
|
||||||
|
background: #060;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
html.light #tt em {
|
||||||
|
color: #d38;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#mtw {
|
#mtw {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -67,7 +157,7 @@ pre, code, a {
|
|||||||
code {
|
code {
|
||||||
font-size: .96em;
|
font-size: .96em;
|
||||||
}
|
}
|
||||||
pre, code {
|
pre, code, tt {
|
||||||
font-family: 'scp', monospace, monospace;
|
font-family: 'scp', monospace, monospace;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -207,7 +297,7 @@ small {
|
|||||||
z-index: 99;
|
z-index: 99;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: monospace, monospace;
|
font-family: 'scp', monospace, monospace;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
line-height: .1em;
|
line-height: .1em;
|
||||||
|
|||||||
@@ -131,18 +131,18 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var btn = document.getElementById("lightswitch");
|
var l = localStorage,
|
||||||
var toggle = function (e) {
|
drk = l.getItem('lightmode') != 1,
|
||||||
if (e) e.preventDefault();
|
btn = document.getElementById("lightswitch"),
|
||||||
var dark = !document.documentElement.getAttribute("class");
|
f = function (e) {
|
||||||
document.documentElement.setAttribute("class", dark ? "dark" : "");
|
if (e) { e.preventDefault(); drk = !drk; }
|
||||||
btn.innerHTML = "go " + (dark ? "light" : "dark");
|
document.documentElement.setAttribute("class", drk? "dark":"light");
|
||||||
if (window.localStorage)
|
btn.innerHTML = "go " + (drk ? "light":"dark");
|
||||||
localStorage.setItem('lightmode', dark ? 0 : 1);
|
l.setItem('lightmode', drk? 0:1);
|
||||||
};
|
};
|
||||||
btn.onclick = toggle;
|
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
btn.onclick = f;
|
||||||
toggle();
|
f();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ function md_plug_err(ex, js) {
|
|||||||
var lns = js.split('\n');
|
var lns = js.split('\n');
|
||||||
if (ln < lns.length) {
|
if (ln < lns.length) {
|
||||||
o = mknod('span');
|
o = mknod('span');
|
||||||
o.style.cssText = 'color:#ac2;font-size:.9em;font-family:scp;display:block';
|
o.style.cssText = "color:#ac2;font-size:.9em;font-family:'scp',monospace,monospace;display:block";
|
||||||
o.textContent = lns[ln - 1];
|
o.textContent = lns[ln - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,13 +84,10 @@ html.dark #save.force-save {
|
|||||||
#save.disabled {
|
#save.disabled {
|
||||||
opacity: .4;
|
opacity: .4;
|
||||||
}
|
}
|
||||||
#helpbox,
|
#helpbox {
|
||||||
#toast {
|
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
border-radius: .4em;
|
border-radius: .4em;
|
||||||
z-index: 9001;
|
z-index: 9001;
|
||||||
}
|
|
||||||
#helpbox {
|
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
@@ -107,19 +104,7 @@ html.dark #save.force-save {
|
|||||||
}
|
}
|
||||||
html.dark #helpbox {
|
html.dark #helpbox {
|
||||||
box-shadow: 0 .5em 2em #444;
|
box-shadow: 0 .5em 2em #444;
|
||||||
}
|
|
||||||
html.dark #helpbox,
|
|
||||||
html.dark #toast {
|
|
||||||
background: #222;
|
background: #222;
|
||||||
border: 1px solid #079;
|
border: 1px solid #079;
|
||||||
border-width: 1px 0;
|
border-width: 1px 0;
|
||||||
}
|
}
|
||||||
#toast {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
padding: .6em 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 30%;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ function Modpoll() {
|
|||||||
|
|
||||||
var skip = null;
|
var skip = null;
|
||||||
|
|
||||||
if (ebi('toast'))
|
if (toast.visible)
|
||||||
skip = 'toast';
|
skip = 'toast';
|
||||||
|
|
||||||
else if (this.skip_one)
|
else if (this.skip_one)
|
||||||
@@ -291,10 +291,9 @@ function Modpoll() {
|
|||||||
"Press F5 or CTRL-R to refresh the page,<br />" +
|
"Press F5 or CTRL-R to refresh the page,<br />" +
|
||||||
"replacing your document with the server copy.",
|
"replacing your document with the server copy.",
|
||||||
|
|
||||||
"You can click this message to ignore and contnue."
|
"You can close this message to ignore and contnue."
|
||||||
];
|
];
|
||||||
return toast(false, "box-shadow:0 1em 2em rgba(64,64,64,0.8);font-weight:normal",
|
return toast.warn(0, "<p>" + msg.join('</p>\n<p>') + '</p>');
|
||||||
36, "<p>" + msg.join('</p>\n<p>') + '</p>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('modpoll eq');
|
console.log('modpoll eq');
|
||||||
@@ -323,16 +322,12 @@ function save(e) {
|
|||||||
var save_btn = ebi("save"),
|
var save_btn = ebi("save"),
|
||||||
save_cls = save_btn.getAttribute('class') + '';
|
save_cls = save_btn.getAttribute('class') + '';
|
||||||
|
|
||||||
if (save_cls.indexOf('disabled') >= 0) {
|
if (save_cls.indexOf('disabled') >= 0)
|
||||||
toast(true, ";font-size:2em;color:#c90", 9, "no changes");
|
return toast.inf(2, "no changes");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var force = (save_cls.indexOf('force-save') >= 0);
|
var force = (save_cls.indexOf('force-save') >= 0);
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
|
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document'))
|
||||||
alert('ok, aborted');
|
return toast.inf(3, 'aborted');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var txt = dom_src.value;
|
var txt = dom_src.value;
|
||||||
|
|
||||||
@@ -357,18 +352,15 @@ function save_cb() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return alert('Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -443,46 +435,10 @@ function savechk_cb() {
|
|||||||
last_modified = this.lastmod;
|
last_modified = this.lastmod;
|
||||||
server_md = this.txt;
|
server_md = this.txt;
|
||||||
draw_md();
|
draw_md();
|
||||||
toast(true, ";font-size:6em;font-family:serif;color:#9b4", 4,
|
toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : ''));
|
||||||
'OK✔️<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>');
|
|
||||||
|
|
||||||
modpoll.disabled = false;
|
modpoll.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toast(autoclose, style, width, msg) {
|
|
||||||
var ok = ebi("toast");
|
|
||||||
if (ok)
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
|
|
||||||
style = "width:" + width + "em;left:calc(50% - " + (width / 2) + "em);" + style;
|
|
||||||
ok = mknod('div');
|
|
||||||
ok.setAttribute('id', 'toast');
|
|
||||||
ok.setAttribute('style', style);
|
|
||||||
ok.innerHTML = msg;
|
|
||||||
var parent = ebi('m');
|
|
||||||
document.documentElement.appendChild(ok);
|
|
||||||
|
|
||||||
var hide = function (delay) {
|
|
||||||
delay = delay || 0;
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.style.opacity = 0;
|
|
||||||
}, delay);
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
if (ok.parentNode)
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
}, delay + 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
ok.onclick = function () {
|
|
||||||
hide(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (autoclose)
|
|
||||||
hide(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// firefox bug: initial selection offset isn't cleared properly through js
|
// firefox bug: initial selection offset isn't cleared properly through js
|
||||||
var ff_clearsel = (function () {
|
var ff_clearsel = (function () {
|
||||||
@@ -761,7 +717,7 @@ function fmt_table(e) {
|
|||||||
|
|
||||||
var ind2 = tab[a].match(re_ind)[0];
|
var ind2 = tab[a].match(re_ind)[0];
|
||||||
if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0]
|
if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0]
|
||||||
return alert(err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]);
|
return toast.err(7, err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]);
|
||||||
|
|
||||||
var t = tab[a].slice(ind.length);
|
var t = tab[a].slice(ind.length);
|
||||||
t = t.replace(re_lpipe, "");
|
t = t.replace(re_lpipe, "");
|
||||||
@@ -771,7 +727,7 @@ function fmt_table(e) {
|
|||||||
if (a == 0)
|
if (a == 0)
|
||||||
ncols = tab[a].length;
|
ncols = tab[a].length;
|
||||||
else if (ncols < tab[a].length)
|
else if (ncols < tab[a].length)
|
||||||
return alert(err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length);
|
return toast.err(7, err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length);
|
||||||
|
|
||||||
// if row has less columns than row2, fill them in
|
// if row has less columns than row2, fill them in
|
||||||
while (tab[a].length < ncols)
|
while (tab[a].length < ncols)
|
||||||
@@ -788,7 +744,7 @@ function fmt_table(e) {
|
|||||||
for (var col = 0; col < tab[1].length; col++) {
|
for (var col = 0; col < tab[1].length; col++) {
|
||||||
var m = tab[1][col].match(re_align);
|
var m = tab[1][col].match(re_align);
|
||||||
if (!m)
|
if (!m)
|
||||||
return alert(err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
|
return toast.err(7, err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
|
||||||
|
|
||||||
if (m[2]) {
|
if (m[2]) {
|
||||||
if (m[1])
|
if (m[1])
|
||||||
@@ -876,10 +832,9 @@ function mark_uni(e) {
|
|||||||
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
|
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
|
||||||
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
||||||
|
|
||||||
if (txt == mod) {
|
if (txt == mod)
|
||||||
alert('no results; no modifications were made');
|
return toast.inf(5, 'no results; no modifications were made');
|
||||||
return;
|
|
||||||
}
|
|
||||||
dom_src.value = mod;
|
dom_src.value = mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,10 +848,9 @@ function iter_uni(e) {
|
|||||||
re = new RegExp('([^' + js_uni_whitelist + ']+)'),
|
re = new RegExp('([^' + js_uni_whitelist + ']+)'),
|
||||||
m = re.exec(txt.slice(ofs));
|
m = re.exec(txt.slice(ofs));
|
||||||
|
|
||||||
if (!m) {
|
if (!m)
|
||||||
alert('no more hits from cursor onwards');
|
return toast.inf(5, 'no more hits from cursor onwards');
|
||||||
return;
|
|
||||||
}
|
|
||||||
ofs += m.index;
|
ofs += m.index;
|
||||||
|
|
||||||
dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward");
|
dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward");
|
||||||
|
|||||||
@@ -30,16 +30,15 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var lightswitch = (function () {
|
var lightswitch = (function () {
|
||||||
var fun = function () {
|
var l = localStorage,
|
||||||
var dark = !document.documentElement.getAttribute("class");
|
drk = l.getItem('lightmode') != 1,
|
||||||
document.documentElement.setAttribute("class", dark ? "dark" : "");
|
f = function (e) {
|
||||||
if (window.localStorage)
|
if (e) drk = !drk;
|
||||||
localStorage.setItem('lightmode', dark ? 0 : 1);
|
document.documentElement.setAttribute("class", drk? "dark":"light");
|
||||||
};
|
l.setItem('lightmode', drk? 0:1);
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
};
|
||||||
fun();
|
f();
|
||||||
|
return f;
|
||||||
return fun;
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -106,15 +106,12 @@ function md_changed(mde, on_srv) {
|
|||||||
|
|
||||||
function save(mde) {
|
function save(mde) {
|
||||||
var save_btn = QS('.editor-toolbar button.save');
|
var save_btn = QS('.editor-toolbar button.save');
|
||||||
if (save_btn.classList.contains('disabled')) {
|
if (save_btn.classList.contains('disabled'))
|
||||||
alert('there is nothing to save');
|
return toast.inf(2, 'no changes');
|
||||||
return;
|
|
||||||
}
|
|
||||||
var force = save_btn.classList.contains('force-save');
|
var force = save_btn.classList.contains('force-save');
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
|
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document'))
|
||||||
alert('ok, aborted');
|
return toast.inf(3, 'aborted');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var txt = mde.value();
|
var txt = mde.value();
|
||||||
|
|
||||||
@@ -138,18 +135,15 @@ function save_cb() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return alert('Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
if (localStorage.getItem('lightmode') != 1)
|
||||||
document.documentElement.setAttribute("class", "dark");
|
document.documentElement.setAttribute("class", "dark");
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -290,8 +290,7 @@ function U2pvis(act, btns) {
|
|||||||
if (this.is_act(this.tab[a].in))
|
if (this.is_act(this.tab[a].in))
|
||||||
console.log("tab %d/%d = sz %s", a, aa, this.tab[a].bt);
|
console.log("tab %d/%d = sz %s", a, aa, this.tab[a].bt);
|
||||||
|
|
||||||
console.log("a");
|
throw new Error('see console');
|
||||||
throw 42;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.innerHTML = fo.hp;
|
obj.innerHTML = fo.hp;
|
||||||
@@ -304,15 +303,8 @@ function U2pvis(act, btns) {
|
|||||||
oldcat = fo.in,
|
oldcat = fo.in,
|
||||||
bz_act = this.act == "bz";
|
bz_act = this.act == "bz";
|
||||||
|
|
||||||
if (oldcat == newcat) {
|
if (oldcat == newcat)
|
||||||
throw 42;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
//console.log("oldcat %s %d, newcat %s %d, head=%d, tail=%d, file=%d, act.old=%s, act.new=%s, bz_act=%s",
|
|
||||||
// oldcat, this.ctr[oldcat],
|
|
||||||
// newcat, this.ctr[newcat],
|
|
||||||
// this.head, this.tail, nfile,
|
|
||||||
// this.is_act(oldcat), this.is_act(newcat), bz_act);
|
|
||||||
|
|
||||||
fo.in = newcat;
|
fo.in = newcat;
|
||||||
this.ctr[oldcat]--;
|
this.ctr[oldcat]--;
|
||||||
@@ -468,6 +460,15 @@ function U2pvis(act, btns) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function fsearch_explain(e) {
|
||||||
|
ev(e);
|
||||||
|
if (!has(perms, 'write'))
|
||||||
|
return alert('your access to this folder is Read-Only\n\n' + (acct == '*' ? 'you are currently not logged in' : 'you are currently logged in as ' + acct));
|
||||||
|
|
||||||
|
alert('you are currently in file-search mode\n\nswitch to upload-mode by clicking the green magnifying glass (next to the big yellow search button), and then refresh\n\nsorry');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function up2k_init(subtle) {
|
function up2k_init(subtle) {
|
||||||
// show modal message
|
// show modal message
|
||||||
function showmodal(msg) {
|
function showmodal(msg) {
|
||||||
@@ -495,8 +496,9 @@ function up2k_init(subtle) {
|
|||||||
shame = 'your browser is impressively ancient';
|
shame = 'your browser is impressively ancient';
|
||||||
|
|
||||||
// upload ui hidden by default, clicking the header shows it
|
// upload ui hidden by default, clicking the header shows it
|
||||||
|
var got_deps = false;
|
||||||
function init_deps() {
|
function init_deps() {
|
||||||
if (!subtle && !window.asmCrypto) {
|
if (!got_deps && !subtle && !window.asmCrypto) {
|
||||||
var fn = 'sha512.' + sha_js + '.js';
|
var fn = 'sha512.' + sha_js + '.js';
|
||||||
showmodal('<h1>loading ' + fn + '</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>');
|
showmodal('<h1>loading ' + fn + '</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>');
|
||||||
import_js('/.cpr/deps/' + fn, unmodal);
|
import_js('/.cpr/deps/' + fn, unmodal);
|
||||||
@@ -507,6 +509,7 @@ function up2k_init(subtle) {
|
|||||||
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance <span style="color:#' +
|
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance <span style="color:#' +
|
||||||
(sha_js == 'ac' ? 'c84">(expecting 20' : '8a5">(but dont worry too much, expect 100') + ' MiB/s)</span>';
|
(sha_js == 'ac' ? 'c84">(expecting 20' : '8a5">(but dont worry too much, expect 100') + ' MiB/s)</span>';
|
||||||
}
|
}
|
||||||
|
got_deps = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// show uploader if the user only has write-access
|
// show uploader if the user only has write-access
|
||||||
@@ -923,8 +926,11 @@ function up2k_init(subtle) {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
clearTimeout(tto);
|
clearTimeout(tto);
|
||||||
|
if (crashed)
|
||||||
|
return defer();
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
while (window['vis_exh']) {
|
while (true) {
|
||||||
var now = Date.now(),
|
var now = Date.now(),
|
||||||
is_busy = 0 !=
|
is_busy = 0 !=
|
||||||
st.todo.head.length +
|
st.todo.head.length +
|
||||||
@@ -1006,7 +1012,7 @@ function up2k_init(subtle) {
|
|||||||
mou_ikkai = true;
|
mou_ikkai = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mou_ikkai)
|
if (!mou_ikkai || crashed)
|
||||||
return defer();
|
return defer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1170,10 +1176,6 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.t_hashed = Date.now();
|
t.t_hashed = Date.now();
|
||||||
if (t.n == 0 && window.location.hash == '#dbg') {
|
|
||||||
var spd = (t.size / ((t.t_hashed - t.t_hashing) / 1000.)) / (1024 * 1024.);
|
|
||||||
alert('{0} ms, {1} MB/s\n'.format(t.t_hashed - t.t_hashing, spd.toFixed(3)) + t.hash.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
pvis.seth(t.n, 2, 'hashing done');
|
pvis.seth(t.n, 2, 'hashing done');
|
||||||
pvis.seth(t.n, 1, '📦 wait');
|
pvis.seth(t.n, 1, '📦 wait');
|
||||||
@@ -1298,8 +1300,8 @@ function up2k_init(subtle) {
|
|||||||
smsg = '';
|
smsg = '';
|
||||||
|
|
||||||
if (!response || !response.hits || !response.hits.length) {
|
if (!response || !response.hits || !response.hits.length) {
|
||||||
msg = 'not found on server';
|
|
||||||
smsg = '404';
|
smsg = '404';
|
||||||
|
msg = 'not found on server <a href="#" onclick="fsearch_explain()" class="fsearch_explain">(explain)</a>';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
smsg = 'found';
|
smsg = 'found';
|
||||||
@@ -1516,7 +1518,7 @@ function up2k_init(subtle) {
|
|||||||
try { orz(xhr); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
|
try { orz(xhr); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
|
||||||
};
|
};
|
||||||
xhr.onerror = function (xev) {
|
xhr.onerror = function (xev) {
|
||||||
if (!window['vis_exh'])
|
if (crashed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
console.log('chunkpit onerror, retrying', t);
|
console.log('chunkpit onerror, retrying', t);
|
||||||
|
|||||||
@@ -87,8 +87,9 @@
|
|||||||
#u2tab td:nth-child(3) {
|
#u2tab td:nth-child(3) {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
}
|
}
|
||||||
#op_up2k.srch #u2tab td:nth-child(3) {
|
#op_up2k.srch td.prog {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
#u2tab tbody tr:hover td {
|
#u2tab tbody tr:hover td {
|
||||||
@@ -245,7 +246,7 @@ html.light #u2foot .warn span {
|
|||||||
margin-bottom: -1em;
|
margin-bottom: -1em;
|
||||||
}
|
}
|
||||||
.prog {
|
.prog {
|
||||||
font-family: monospace;
|
font-family: monospace, monospace;
|
||||||
}
|
}
|
||||||
#u2tab a>span {
|
#u2tab a>span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -257,6 +258,11 @@ html.light #u2foot .warn span {
|
|||||||
float: right;
|
float: right;
|
||||||
margin-bottom: -.3em;
|
margin-bottom: -.3em;
|
||||||
}
|
}
|
||||||
|
.fsearch_explain {
|
||||||
|
padding-left: .7em;
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ if (!window['console'])
|
|||||||
|
|
||||||
|
|
||||||
var is_touch = 'ontouchstart' in window,
|
var is_touch = 'ontouchstart' in window,
|
||||||
ANDROID = /(android)/i.test(navigator.userAgent);
|
IPHONE = /iPhone|iPad|iPod/i.test(navigator.userAgent),
|
||||||
|
ANDROID = /android/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
|
var ebi = document.getElementById.bind(document),
|
||||||
|
QS = document.querySelector.bind(document),
|
||||||
|
QSA = document.querySelectorAll.bind(document),
|
||||||
|
mknod = document.createElement.bind(document);
|
||||||
|
|
||||||
|
|
||||||
// error handler for mobile devices
|
// error handler for mobile devices
|
||||||
@@ -21,36 +28,60 @@ function esc(txt) {
|
|||||||
}[c];
|
}[c];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
var crashed = false, ignexd = {};
|
||||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||||
if (!window.onerror)
|
if ((msg + '').indexOf('ResizeObserver') !== -1)
|
||||||
|
return; // chrome issue 809574 (benign, from <video>)
|
||||||
|
|
||||||
|
var ekey = url + '\n' + lineNo + '\n' + msg;
|
||||||
|
if (ignexd[ekey] || crashed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
crashed = true;
|
||||||
window.onerror = undefined;
|
window.onerror = undefined;
|
||||||
window['vis_exh'] = null;
|
var html = ['<h1>you hit a bug!</h1><p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a></p><p>please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code><br /> (and if you can, press F12 and include the "Console" tab in the screenshot too)</p><p>',
|
||||||
var html = ['<h1>you hit a bug!</h1><p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();" style="text-decoration:underline;color:#fc0">reset copyparty settings</a> if you are stuck here</p><p>please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code><br /> (and if you can, press F12 and include the "Console" tab in the screenshot too)</p><p>',
|
|
||||||
esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>'];
|
esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>'];
|
||||||
|
|
||||||
if (error) {
|
try {
|
||||||
var find = ['desc', 'stack', 'trace'];
|
if (error) {
|
||||||
for (var a = 0; a < find.length; a++)
|
var find = ['desc', 'stack', 'trace'];
|
||||||
if (String(error[find[a]]) !== 'undefined')
|
for (var a = 0; a < find.length; a++)
|
||||||
html.push('<h3>' + find[a] + '</h3>' +
|
if (String(error[find[a]]) !== 'undefined')
|
||||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
html.push('<h3>' + find[a] + '</h3>' +
|
||||||
|
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
||||||
|
}
|
||||||
|
ignexd[ekey] = true;
|
||||||
|
html.push('<h3>localStore</h3>' + esc(JSON.stringify(localStorage)));
|
||||||
}
|
}
|
||||||
document.body.innerHTML = html.join('\n');
|
catch (e) { }
|
||||||
|
|
||||||
var s = mknod('style');
|
try {
|
||||||
s.innerHTML = 'body{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em} h1{margin:.5em 1em 0 0;padding:0} h3{border-top:1px solid #999;margin:0} code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} *{line-height:1.5em}';
|
var exbox = ebi('exbox');
|
||||||
document.head.appendChild(s);
|
if (!exbox) {
|
||||||
|
exbox = mknod('div');
|
||||||
|
exbox.setAttribute('id', 'exbox');
|
||||||
|
document.body.appendChild(exbox);
|
||||||
|
|
||||||
|
var s = mknod('style');
|
||||||
|
s.innerHTML = '#exbox{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%} #exbox h1{margin:.5em 1em 0 0;padding:0} #exbox h3{border-top:1px solid #999;margin:1em 0 0 0} #exbox a{text-decoration:underline;color:#fc0} #exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} #exbox *{line-height:1.5em}';
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
exbox.innerHTML = html.join('\n');
|
||||||
|
exbox.style.display = 'block';
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
document.body.innerHTML = html.join('\n');
|
||||||
|
}
|
||||||
throw 'fatal_err';
|
throw 'fatal_err';
|
||||||
}
|
}
|
||||||
|
function ignex(all) {
|
||||||
|
var o = ebi('exbox');
|
||||||
var ebi = document.getElementById.bind(document),
|
o.style.display = 'none';
|
||||||
QS = document.querySelector.bind(document),
|
o.innerHTML = '';
|
||||||
QSA = document.querySelectorAll.bind(document),
|
crashed = false;
|
||||||
mknod = document.createElement.bind(document);
|
if (!all)
|
||||||
|
window.onerror = vis_exh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function ctrl(e) {
|
function ctrl(e) {
|
||||||
@@ -92,6 +123,15 @@ if (!String.startsWith) {
|
|||||||
return this.substring(i, i + s.length) === s;
|
return this.substring(i, i + s.length) === s;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!Element.prototype.closest) {
|
||||||
|
Element.prototype.closest = function (s) {
|
||||||
|
var el = this;
|
||||||
|
do {
|
||||||
|
if (el.msMatchesSelector(s)) return el;
|
||||||
|
el = el.parentElement || el.parentNode;
|
||||||
|
} while (el !== null && el.nodeType === 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/950146
|
// https://stackoverflow.com/a/950146
|
||||||
@@ -321,6 +361,18 @@ function linksplit(rp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function vsplit(vp) {
|
||||||
|
if (vp.endsWith('/'))
|
||||||
|
vp = vp.slice(0, -1);
|
||||||
|
|
||||||
|
var ofs = vp.lastIndexOf('/') + 1,
|
||||||
|
base = vp.slice(0, ofs),
|
||||||
|
fn = vp.slice(ofs);
|
||||||
|
|
||||||
|
return [base, fn];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function uricom_enc(txt, do_fb_enc) {
|
function uricom_enc(txt, do_fb_enc) {
|
||||||
try {
|
try {
|
||||||
return encodeURIComponent(txt);
|
return encodeURIComponent(txt);
|
||||||
@@ -407,19 +459,14 @@ function jcp(obj) {
|
|||||||
|
|
||||||
|
|
||||||
function sread(key) {
|
function sread(key) {
|
||||||
if (window.localStorage)
|
return localStorage.getItem(key);
|
||||||
return localStorage.getItem(key);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function swrite(key, val) {
|
function swrite(key, val) {
|
||||||
if (window.localStorage) {
|
if (val === undefined || val === null)
|
||||||
if (val === undefined || val === null)
|
localStorage.removeItem(key);
|
||||||
localStorage.removeItem(key);
|
else
|
||||||
else
|
localStorage.setItem(key, val);
|
||||||
localStorage.setItem(key, val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function jread(key, fb) {
|
function jread(key, fb) {
|
||||||
@@ -503,13 +550,19 @@ var tt = (function () {
|
|||||||
var r = {
|
var r = {
|
||||||
"tt": mknod("div"),
|
"tt": mknod("div"),
|
||||||
"en": true,
|
"en": true,
|
||||||
"el": null
|
"el": null,
|
||||||
|
"skip": false
|
||||||
};
|
};
|
||||||
|
|
||||||
r.tt.setAttribute('id', 'tt');
|
r.tt.setAttribute('id', 'tt');
|
||||||
document.body.appendChild(r.tt);
|
document.body.appendChild(r.tt);
|
||||||
|
|
||||||
r.show = function () {
|
r.show = function () {
|
||||||
|
if (r.skip) {
|
||||||
|
r.skip = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var cfg = sread('tooltips');
|
var cfg = sread('tooltips');
|
||||||
if (cfg !== null && cfg != '1')
|
if (cfg !== null && cfg != '1')
|
||||||
return;
|
return;
|
||||||
@@ -541,12 +594,25 @@ var tt = (function () {
|
|||||||
clmod(r.tt, 'show', 1);
|
clmod(r.tt, 'show', 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
r.hide = function () {
|
r.hide = function (e) {
|
||||||
|
ev(e);
|
||||||
clmod(r.tt, 'show');
|
clmod(r.tt, 'show');
|
||||||
if (r.el)
|
if (r.el)
|
||||||
r.el.removeEventListener('mouseleave', r.hide);
|
r.el.removeEventListener('mouseleave', r.hide);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (is_touch && IPHONE) {
|
||||||
|
var f1 = r.show,
|
||||||
|
f2 = r.hide;
|
||||||
|
|
||||||
|
r.show = function () {
|
||||||
|
setTimeout(f1.bind(this), 301);
|
||||||
|
};
|
||||||
|
r.hide = function () {
|
||||||
|
setTimeout(f2.bind(this), 301);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
r.tt.onclick = r.hide;
|
r.tt.onclick = r.hide;
|
||||||
|
|
||||||
r.att = function (ctr) {
|
r.att = function (ctr) {
|
||||||
@@ -579,3 +645,54 @@ var tt = (function () {
|
|||||||
|
|
||||||
return r;
|
return r;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var toast = (function () {
|
||||||
|
var r = {},
|
||||||
|
te = null,
|
||||||
|
visible = false,
|
||||||
|
obj = mknod('div');
|
||||||
|
|
||||||
|
obj.setAttribute('id', 'toast');
|
||||||
|
document.body.appendChild(obj);;
|
||||||
|
|
||||||
|
r.hide = function (e) {
|
||||||
|
ev(e);
|
||||||
|
clearTimeout(te);
|
||||||
|
clmod(obj, 'vis');
|
||||||
|
r.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
r.show = function (cl, ms, txt) {
|
||||||
|
clearTimeout(te);
|
||||||
|
if (ms)
|
||||||
|
te = setTimeout(r.hide, ms * 1000);
|
||||||
|
|
||||||
|
var html = '', hp = txt.split(/(?=<.?pre>)/i);
|
||||||
|
for (var a = 0; a < hp.length; a++)
|
||||||
|
html += hp[a].startsWith('<pre>') ? hp[a] :
|
||||||
|
hp[a].replace(/<br ?.?>\n/g, '\n').replace(/\n<br ?.?>/g, '\n').replace(/\n/g, '<br />\n');
|
||||||
|
|
||||||
|
obj.innerHTML = '<a href="#" id="toastc">x</a>' + html;
|
||||||
|
obj.className = cl;
|
||||||
|
ms += obj.offsetWidth;
|
||||||
|
obj.className += ' vis';
|
||||||
|
ebi('toastc').onclick = r.hide;
|
||||||
|
r.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
r.ok = function (ms, txt) {
|
||||||
|
r.show('ok', ms, txt);
|
||||||
|
};
|
||||||
|
r.inf = function (ms, txt) {
|
||||||
|
r.show('inf', ms, txt);
|
||||||
|
};
|
||||||
|
r.warn = function (ms, txt) {
|
||||||
|
r.show('warn', ms, txt);
|
||||||
|
};
|
||||||
|
r.err = function (ms, txt) {
|
||||||
|
r.show('err', ms, txt);
|
||||||
|
};
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|||||||
@@ -10,19 +10,25 @@ u k:k
|
|||||||
# share "." (the current directory)
|
# share "." (the current directory)
|
||||||
# as "/" (the webroot) for the following users:
|
# as "/" (the webroot) for the following users:
|
||||||
# "r" grants read-access for anyone
|
# "r" grants read-access for anyone
|
||||||
# "a ed" grants read-write to ed
|
# "rw ed" grants read-write to ed
|
||||||
.
|
.
|
||||||
/
|
/
|
||||||
r
|
r
|
||||||
a ed
|
rw ed
|
||||||
|
|
||||||
# custom permissions for the "priv" folder:
|
# custom permissions for the "priv" folder:
|
||||||
# user "k" can see/read the contents
|
# user "k" can only see/read the contents
|
||||||
# and "ed" gets read-write access
|
# user "ed" gets read-write access
|
||||||
./priv
|
./priv
|
||||||
/priv
|
/priv
|
||||||
r k
|
r k
|
||||||
a ed
|
rw ed
|
||||||
|
|
||||||
|
# this does the same thing:
|
||||||
|
./priv
|
||||||
|
/priv
|
||||||
|
r ed k
|
||||||
|
w ed
|
||||||
|
|
||||||
# share /home/ed/Music/ as /music and let anyone read it
|
# share /home/ed/Music/ as /music and let anyone read it
|
||||||
# (this will replace any folder called "music" in the webroot)
|
# (this will replace any folder called "music" in the webroot)
|
||||||
@@ -41,5 +47,5 @@ c e2d
|
|||||||
c nodupe
|
c nodupe
|
||||||
|
|
||||||
# this entire config file can be replaced with these arguments:
|
# this entire config file can be replaced with these arguments:
|
||||||
# -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w
|
# -u ed:123 -u k:k -v .::r:a,ed -v priv:priv:r,k:rw,ed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w:c,e2d:c,nodupe
|
||||||
# but note that the config file always wins in case of conflicts
|
# but note that the config file always wins in case of conflicts
|
||||||
|
|||||||
@@ -166,7 +166,10 @@ dbg.asyncStore.pendingBreakpoints = {}
|
|||||||
about:config >> devtools.debugger.prefs-schema-version = -1
|
about:config >> devtools.debugger.prefs-schema-version = -1
|
||||||
|
|
||||||
# determine server version
|
# determine server version
|
||||||
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser,up2k}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
||||||
|
|
||||||
|
# download all sfx versions
|
||||||
|
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | while read v t; do fn="copyparty $v $t.py"; [ -e $fn ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ echo
|
|||||||
#
|
#
|
||||||
# `no-cm` saves ~90k by removing easymde/codemirror
|
# `no-cm` saves ~90k by removing easymde/codemirror
|
||||||
# (the fancy markdown editor)
|
# (the fancy markdown editor)
|
||||||
|
#
|
||||||
|
# `no-fnt` saves ~9k by removing the source-code-pro font
|
||||||
|
# (mainly used my the markdown viewer/editor)
|
||||||
|
#
|
||||||
|
# `no-dd` saves ~2k by removing the mouse cursor
|
||||||
|
|
||||||
|
|
||||||
# port install gnutar findutils gsed coreutils
|
# port install gnutar findutils gsed coreutils
|
||||||
@@ -57,14 +62,18 @@ use_gz=
|
|||||||
do_sh=1
|
do_sh=1
|
||||||
do_py=1
|
do_py=1
|
||||||
while [ ! -z "$1" ]; do
|
while [ ! -z "$1" ]; do
|
||||||
[ "$1" = clean ] && clean=1 && shift && continue
|
case $1 in
|
||||||
[ "$1" = re ] && repack=1 && shift && continue
|
clean) clean=1 ; ;;
|
||||||
[ "$1" = gz ] && use_gz=1 && shift && continue
|
re) repack=1 ; ;;
|
||||||
[ "$1" = no-ogv ] && no_ogv=1 && shift && continue
|
gz) use_gz=1 ; ;;
|
||||||
[ "$1" = no-cm ] && no_cm=1 && shift && continue
|
no-ogv) no_ogv=1 ; ;;
|
||||||
[ "$1" = no-sh ] && do_sh= && shift && continue
|
no-fnt) no_fnt=1 ; ;;
|
||||||
[ "$1" = no-py ] && do_py= && shift && continue
|
no-dd) no_dd=1 ; ;;
|
||||||
break
|
no-cm) no_cm=1 ; ;;
|
||||||
|
no-sh) do_sh= ; ;;
|
||||||
|
no-py) do_py= ; ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
tmv() {
|
tmv() {
|
||||||
@@ -190,6 +199,18 @@ done
|
|||||||
sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
|
sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ $no_fnt ] && {
|
||||||
|
rm -f copyparty/web/deps/scp.woff2
|
||||||
|
f=copyparty/web/md.css
|
||||||
|
sed -r '/scp\.woff2/d' <$f >t && tmv "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ $no_dd ] && {
|
||||||
|
rm -rf copyparty/web/dd
|
||||||
|
f=copyparty/web/browser.css
|
||||||
|
sed -r 's/(cursor: )url\([^)]+\), (pointer)/\1\2/; /[0-9]+% \{cursor:/d; /animation: cursor/d' <$f >t && tmv "$f"
|
||||||
|
}
|
||||||
|
|
||||||
[ $repack ] ||
|
[ $repack ] ||
|
||||||
find | grep -E '\.py$' |
|
find | grep -E '\.py$' |
|
||||||
grep -vE '__version__' |
|
grep -vE '__version__' |
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ class Cfg(Namespace):
|
|||||||
rproxy=0,
|
rproxy=0,
|
||||||
ed=False,
|
ed=False,
|
||||||
nw=False,
|
nw=False,
|
||||||
|
no_mv=False,
|
||||||
|
no_del=False,
|
||||||
no_zip=False,
|
no_zip=False,
|
||||||
|
no_voldump=True,
|
||||||
no_scandir=False,
|
no_scandir=False,
|
||||||
no_sendfile=True,
|
no_sendfile=True,
|
||||||
no_rescan=True,
|
no_rescan=True,
|
||||||
@@ -90,7 +93,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
if not vol.startswith(top):
|
if not vol.startswith(top):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mode = vol[-2]
|
mode = vol[-2].replace("a", "rwmd")
|
||||||
usr = vol[-1]
|
usr = vol[-1]
|
||||||
if usr == "a":
|
if usr == "a":
|
||||||
usr = ""
|
usr = ""
|
||||||
@@ -99,7 +102,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
vol += "/"
|
vol += "/"
|
||||||
|
|
||||||
top, sub = vol.split("/", 1)
|
top, sub = vol.split("/", 1)
|
||||||
vcfg.append("{0}/{1}:{1}:{2}{3}".format(top, sub, mode, usr))
|
vcfg.append("{0}/{1}:{1}:{2},{3}".format(top, sub, mode, usr))
|
||||||
|
|
||||||
pprint.pprint(vcfg)
|
pprint.pprint(vcfg)
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Cfg(Namespace):
|
|||||||
"hist": None,
|
"hist": None,
|
||||||
"no_hash": False,
|
"no_hash": False,
|
||||||
"css_browser": None,
|
"css_browser": None,
|
||||||
|
"no_voldump": True,
|
||||||
"rproxy": 0,
|
"rproxy": 0,
|
||||||
}
|
}
|
||||||
ex.update(ex2)
|
ex.update(ex2)
|
||||||
@@ -57,8 +58,8 @@ class TestVFS(unittest.TestCase):
|
|||||||
# type: (VFS, str, str) -> tuple[str, str, str]
|
# type: (VFS, str, str) -> tuple[str, str, str]
|
||||||
"""helper for resolving and listing a folder"""
|
"""helper for resolving and listing a folder"""
|
||||||
vn, rem = vfs.get(vpath, uname, True, False)
|
vn, rem = vfs.get(vpath, uname, True, False)
|
||||||
r1 = vn.ls(rem, uname, False)
|
r1 = vn.ls(rem, uname, False, [[True]])
|
||||||
r2 = vn.ls(rem, uname, False)
|
r2 = vn.ls(rem, uname, False, [[True]])
|
||||||
self.assertEqual(r1, r2)
|
self.assertEqual(r1, r2)
|
||||||
|
|
||||||
fsdir, real, virt = r1
|
fsdir, real, virt = r1
|
||||||
@@ -68,6 +69,11 @@ class TestVFS(unittest.TestCase):
|
|||||||
def log(self, src, msg, c=0):
|
def log(self, src, msg, c=0):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def assertAxs(self, dct, lst):
|
||||||
|
t1 = list(sorted(dct.keys()))
|
||||||
|
t2 = list(sorted(lst))
|
||||||
|
self.assertEqual(t1, t2)
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
td = os.path.join(self.td, "vfs")
|
td = os.path.join(self.td, "vfs")
|
||||||
os.mkdir(td)
|
os.mkdir(td)
|
||||||
@@ -88,53 +94,53 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, ["*"])
|
self.assertAxs(vfs.axs.uwrite, ["*"])
|
||||||
|
|
||||||
# single read-only rootfs (relative path)
|
# single read-only rootfs (relative path)
|
||||||
vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs
|
vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertAxs(vfs.axs.uwrite, [])
|
||||||
|
|
||||||
# single read-only rootfs (absolute path)
|
# single read-only rootfs (absolute path)
|
||||||
vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs
|
vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertAxs(vfs.axs.uwrite, [])
|
||||||
|
|
||||||
# read-only rootfs with write-only subdirectory (read-write for k)
|
# read-only rootfs with write-only subdirectory (read-write for k)
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
Cfg(a=["k:k"], v=[".::r:ak", "a/ac/acb:a/ac/acb:w:ak"]),
|
Cfg(a=["k:k"], v=[".::r:rw,k", "a/ac/acb:a/ac/acb:w:rw,k"]),
|
||||||
self.log,
|
self.log,
|
||||||
).vfs
|
).vfs
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["*", "k"])
|
self.assertAxs(vfs.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(vfs.uwrite, ["k"])
|
self.assertAxs(vfs.axs.uwrite, ["k"])
|
||||||
n = vfs.nodes["a"]
|
n = vfs.nodes["a"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a")
|
self.assertEqual(n.vpath, "a")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertAxs(n.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertAxs(n.axs.uwrite, ["k"])
|
||||||
n = n.nodes["ac"]
|
n = n.nodes["ac"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a/ac")
|
self.assertEqual(n.vpath, "a/ac")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertAxs(n.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertAxs(n.axs.uwrite, ["k"])
|
||||||
n = n.nodes["acb"]
|
n = n.nodes["acb"]
|
||||||
self.assertEqual(n.nodes, {})
|
self.assertEqual(n.nodes, {})
|
||||||
self.assertEqual(n.vpath, "a/ac/acb")
|
self.assertEqual(n.vpath, "a/ac/acb")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
|
||||||
self.assertEqual(n.uread, ["k"])
|
self.assertAxs(n.axs.uread, ["k"])
|
||||||
self.assertEqual(n.uwrite, ["*", "k"])
|
self.assertAxs(n.axs.uwrite, ["*", "k"])
|
||||||
|
|
||||||
# something funky about the windows path normalization,
|
# something funky about the windows path normalization,
|
||||||
# doesn't really matter but makes the test messy, TODO?
|
# doesn't really matter but makes the test messy, TODO?
|
||||||
@@ -173,24 +179,24 @@ class TestVFS(unittest.TestCase):
|
|||||||
|
|
||||||
# admin-only rootfs with all-read-only subfolder
|
# admin-only rootfs with all-read-only subfolder
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
Cfg(a=["k:k"], v=[".::ak", "a:a:r"]),
|
Cfg(a=["k:k"], v=[".::rw,k", "a:a:r"]),
|
||||||
self.log,
|
self.log,
|
||||||
).vfs
|
).vfs
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["k"])
|
self.assertAxs(vfs.axs.uread, ["k"])
|
||||||
self.assertEqual(vfs.uwrite, ["k"])
|
self.assertAxs(vfs.axs.uwrite, ["k"])
|
||||||
n = vfs.nodes["a"]
|
n = vfs.nodes["a"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a")
|
self.assertEqual(n.vpath, "a")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*"])
|
self.assertAxs(n.axs.uread, ["*"])
|
||||||
self.assertEqual(n.uwrite, [])
|
self.assertAxs(n.axs.uwrite, [])
|
||||||
self.assertEqual(vfs.can_access("/", "*"), [False, False])
|
self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False])
|
||||||
self.assertEqual(vfs.can_access("/", "k"), [True, True])
|
self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False])
|
||||||
self.assertEqual(vfs.can_access("/a", "*"), [True, False])
|
self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False])
|
||||||
self.assertEqual(vfs.can_access("/a", "k"), [True, False])
|
self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False])
|
||||||
|
|
||||||
# breadth-first construction
|
# breadth-first construction
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
@@ -247,26 +253,26 @@ class TestVFS(unittest.TestCase):
|
|||||||
./src
|
./src
|
||||||
/dst
|
/dst
|
||||||
r a
|
r a
|
||||||
a asd
|
rw asd
|
||||||
"""
|
"""
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
)
|
)
|
||||||
|
|
||||||
au = AuthSrv(Cfg(c=[cfg_path]), self.log)
|
au = AuthSrv(Cfg(c=[cfg_path]), self.log)
|
||||||
self.assertEqual(au.user["a"], "123")
|
self.assertEqual(au.acct["a"], "123")
|
||||||
self.assertEqual(au.user["asd"], "fgh:jkl")
|
self.assertEqual(au.acct["asd"], "fgh:jkl")
|
||||||
n = au.vfs
|
n = au.vfs
|
||||||
# root was not defined, so PWD with no access to anyone
|
# root was not defined, so PWD with no access to anyone
|
||||||
self.assertEqual(n.vpath, "")
|
self.assertEqual(n.vpath, "")
|
||||||
self.assertEqual(n.realpath, None)
|
self.assertEqual(n.realpath, None)
|
||||||
self.assertEqual(n.uread, [])
|
self.assertAxs(n.axs.uread, [])
|
||||||
self.assertEqual(n.uwrite, [])
|
self.assertAxs(n.axs.uwrite, [])
|
||||||
self.assertEqual(len(n.nodes), 1)
|
self.assertEqual(len(n.nodes), 1)
|
||||||
n = n.nodes["dst"]
|
n = n.nodes["dst"]
|
||||||
self.assertEqual(n.vpath, "dst")
|
self.assertEqual(n.vpath, "dst")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "src"))
|
self.assertEqual(n.realpath, os.path.join(td, "src"))
|
||||||
self.assertEqual(n.uread, ["a", "asd"])
|
self.assertAxs(n.axs.uread, ["a", "asd"])
|
||||||
self.assertEqual(n.uwrite, ["asd"])
|
self.assertAxs(n.axs.uwrite, ["asd"])
|
||||||
self.assertEqual(len(n.nodes), 0)
|
self.assertEqual(len(n.nodes), 0)
|
||||||
|
|
||||||
os.unlink(cfg_path)
|
os.unlink(cfg_path)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ if MACOS:
|
|||||||
from copyparty.util import Unrecv
|
from copyparty.util import Unrecv
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
stdout = stdout.decode("utf-8")
|
stdout = stdout.decode("utf-8")
|
||||||
@@ -39,8 +39,8 @@ def runcmd(*argv):
|
|||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
def chkcmd(*argv):
|
def chkcmd(argv):
|
||||||
ok, sout, serr = runcmd(*argv)
|
ok, sout, serr = runcmd(argv)
|
||||||
if ok != 0:
|
if ok != 0:
|
||||||
raise Exception(serr)
|
raise Exception(serr)
|
||||||
|
|
||||||
@@ -60,12 +60,12 @@ def get_ramdisk():
|
|||||||
|
|
||||||
if os.path.exists("/Volumes"):
|
if os.path.exists("/Volumes"):
|
||||||
# hdiutil eject /Volumes/cptd/
|
# hdiutil eject /Volumes/cptd/
|
||||||
devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://131072")
|
devname, _ = chkcmd("hdiutil attach -nomount ram://131072".split())
|
||||||
devname = devname.strip()
|
devname = devname.strip()
|
||||||
print("devname: [{}]".format(devname))
|
print("devname: [{}]".format(devname))
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
try:
|
try:
|
||||||
_, _ = chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname)
|
_, _ = chkcmd(["diskutil", "eraseVolume", "HFS+", "cptd", devname])
|
||||||
with open("/Volumes/cptd/.metadata_never_index", "w") as f:
|
with open("/Volumes/cptd/.metadata_never_index", "w") as f:
|
||||||
f.write("orz")
|
f.write("orz")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user