mirror of
https://github.com/9001/copyparty.git
synced 2025-10-29 02:53:47 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02910b0020 | ||
|
|
23b8901c9c | ||
|
|
99f6ed0cd7 | ||
|
|
890c310880 | ||
|
|
0194eeb31f | ||
|
|
f9be4c62b1 | ||
|
|
027e8c18f1 | ||
|
|
4a3bb35a95 | ||
|
|
4bfb0d4494 | ||
|
|
7e0ef03a1e | ||
|
|
f7dbd95a54 | ||
|
|
515ee2290b | ||
|
|
b0c78910bb | ||
|
|
f4ca62b664 | ||
|
|
8eb8043a3d | ||
|
|
3e8541362a | ||
|
|
789724e348 | ||
|
|
5125b9532f | ||
|
|
ebc9de02b0 | ||
|
|
ec788fa491 | ||
|
|
9b5e264574 | ||
|
|
57c297274b | ||
|
|
e9bf092317 | ||
|
|
d173887324 | ||
|
|
99820d854c | ||
|
|
62df0a0eb2 | ||
|
|
600e9ac947 | ||
|
|
3ca41be2b4 | ||
|
|
5c7debd900 | ||
|
|
7fa5b23ce3 | ||
|
|
ff82738aaf | ||
|
|
bf5ee9d643 | ||
|
|
72a8593ecd | ||
|
|
bc3bbe07d4 | ||
|
|
c7cb64bfef | ||
|
|
629f537d06 | ||
|
|
9e988041b8 | ||
|
|
f9a8b5c9d7 | ||
|
|
b9c3538253 | ||
|
|
2bc0cdf017 | ||
|
|
02a91f60d4 | ||
|
|
fae83da197 | ||
|
|
0fe4aa6418 | ||
|
|
21a51bf0dc | ||
|
|
bcb353cc30 | ||
|
|
6af4508518 | ||
|
|
6a559bc28a | ||
|
|
0f5026cd20 | ||
|
|
a91b80a311 | ||
|
|
ec534701c8 | ||
|
|
af5169f67f | ||
|
|
18676c5e65 | ||
|
|
e2df6fda7b | ||
|
|
e9ae9782fe | ||
|
|
016dba4ca9 | ||
|
|
39c7ef305f | ||
|
|
849c1dc848 | ||
|
|
61414014fe | ||
|
|
578a915884 | ||
|
|
eacafb8a63 | ||
|
|
4446760f74 | ||
|
|
6da2a083f9 | ||
|
|
8837c8f822 | ||
|
|
bac301ed66 | ||
|
|
061db3906d |
5
.vscode/tasks.json
vendored
5
.vscode/tasks.json
vendored
@@ -9,7 +9,10 @@
|
||||
{
|
||||
"label": "no_dbg",
|
||||
"type": "shell",
|
||||
"command": "${config:python.pythonPath} .vscode/launch.py"
|
||||
"command": "${config:python.pythonPath}",
|
||||
"args": [
|
||||
".vscode/launch.py"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
121
README.md
121
README.md
@@ -59,6 +59,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
* [just the sfx](#just-the-sfx)
|
||||
* [complete release](#complete-release)
|
||||
* [todo](#todo)
|
||||
* [discarded ideas](#discarded-ideas)
|
||||
|
||||
|
||||
## quickstart
|
||||
@@ -69,7 +70,7 @@ running the sfx without arguments (for example doubleclicking it on Windows) wil
|
||||
|
||||
some recommended options:
|
||||
* `-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`
|
||||
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||
* replace `:r:afoo` with `:rfoo` to only make the folder readable by `foo` and nobody else
|
||||
@@ -114,7 +115,7 @@ summary: all planned features work! now please enjoy the bloatening
|
||||
|
||||
* backend stuff
|
||||
* ☑ sanic multipart parser
|
||||
* ☑ load balancer (multiprocessing)
|
||||
* ☑ multiprocessing (actual multithreading)
|
||||
* ☑ volumes (mountpoints)
|
||||
* ☑ accounts
|
||||
* upload
|
||||
@@ -130,10 +131,10 @@ summary: all planned features work! now please enjoy the bloatening
|
||||
* ☑ tree-view
|
||||
* ☑ audio player (with OS media controls)
|
||||
* ☑ thumbnails
|
||||
* ☑ images using Pillow
|
||||
* ☑ videos using FFmpeg
|
||||
* ☑ ...of images using Pillow
|
||||
* ☑ ...of videos using FFmpeg
|
||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||
* ☑ image gallery
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ SPA (browse while uploading)
|
||||
* if you use the file-tree on the left only, not folders in the file list
|
||||
* server indexing
|
||||
@@ -154,10 +155,10 @@ small collection of user feedback
|
||||
|
||||
# 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 handle filenames with mojibake
|
||||
* MacOS: `--th-ff-jpg` may fix thumbnails using macports-FFmpeg
|
||||
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions
|
||||
|
||||
## general bugs
|
||||
|
||||
@@ -172,9 +173,12 @@ small collection of user feedback
|
||||
* Windows: folders cannot be accessed if the name ends with `.`
|
||||
* python or windows bug
|
||||
|
||||
* 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
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
# the browser
|
||||
|
||||
@@ -188,34 +192,46 @@ small collection of user feedback
|
||||
* `[📂]` mkdir, create directories
|
||||
* `[📝]` new-md, create a new markdown document
|
||||
* `[📟]` send-msg, either to server-log or into textfiles if `--urlform save`
|
||||
* `[⚙️]` client configuration options
|
||||
* `[🎺]` audio-player config options
|
||||
* `[⚙️]` general client config options
|
||||
|
||||
|
||||
## hotkeys
|
||||
|
||||
the browser has the following hotkeys
|
||||
the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
||||
* `B` toggle breadcrumbs / directory tree
|
||||
* `I/K` prev/next folder
|
||||
* `M` parent folder
|
||||
* `M` parent folder (or unexpand current)
|
||||
* `G` toggle list / grid view
|
||||
* `T` toggle thumbnails / icons
|
||||
* when playing audio:
|
||||
* `J/L` prev/next song
|
||||
* `U/O` skip 10sec back/forward
|
||||
* `0..9` jump to 10%..90%
|
||||
* `0..9` jump to 0%..90%
|
||||
* `P` play/pause (also starts playing the folder)
|
||||
* when viewing images / playing videos:
|
||||
* `J/L, Left/Right` prev/next file
|
||||
* `Home/End` first/last file
|
||||
* `U/O` skip 10sec back/forward
|
||||
* `P/K/Space` play/pause video
|
||||
* `Esc` close viewer
|
||||
* videos:
|
||||
* `U/O` skip 10sec back/forward
|
||||
* `P/K/Space` play/pause
|
||||
* `F` fullscreen
|
||||
* `C` continue playing next video
|
||||
* `R` loop
|
||||
* `M` mute
|
||||
* when tree-sidebar is open:
|
||||
* `A/D` adjust tree width
|
||||
* in the grid view:
|
||||
* `S` toggle multiselect
|
||||
* shift+`A/D` zoom
|
||||
|
||||
* in the markdown editor:
|
||||
* `^s` save
|
||||
* `^h` header
|
||||
* `^k` autoformat table
|
||||
* `^u` jump to next unicode character
|
||||
* `^e` toggle editor / preview
|
||||
* `^up, ^down` jump paragraphs
|
||||
|
||||
## tree-mode
|
||||
|
||||
@@ -228,9 +244,9 @@ click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` tog
|
||||
|
||||

|
||||
|
||||
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how destructive your users are
|
||||
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
|
||||
|
||||
images named `folder.jpg` and `folder.png` become the thumbnail of the folder they're in
|
||||
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
|
||||
|
||||
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
|
||||
|
||||
@@ -247,9 +263,10 @@ the `zip` link next to folders can produce various types of zip/tar files using
|
||||
| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |
|
||||
|
||||
* hidden files (dotfiles) are excluded unless `-ed`
|
||||
* the up2k.db is always excluded
|
||||
* `up2k.db` and `dir.txt` is always excluded
|
||||
* `zip_crc` will take longer to download since the server has to read each file twice
|
||||
* please let me know if you find a program old enough to actually need this
|
||||
* this is only to support MS-DOS PKZIP v2.04g (october 1993) and older
|
||||
* how are you accessing copyparty actually
|
||||
|
||||
you can also zip a selection of files or folders by clicking them in the browser, that brings up a selection editor and zip button in the bottom right
|
||||
|
||||
@@ -264,9 +281,11 @@ two upload methods are available in the html client:
|
||||
up2k has several advantages:
|
||||
* you can drop folders into the browser (files are added recursively)
|
||||
* files are processed in chunks, and each chunk is checksummed
|
||||
* uploads resume if they are interrupted (for example by a reboot)
|
||||
* uploads autoresume if they are interrupted by network issues
|
||||
* uploads resume if you reboot your browser or pc, just upload the same files again
|
||||
* server detects any corruption; the client reuploads affected chunks
|
||||
* the client doesn't upload anything that already exists on the server
|
||||
* much higher speeds than ftp/scp/tarpipe on some internet connections (mainly american ones) thanks to parallel connections
|
||||
* the last-modified timestamp of the file is preserved
|
||||
|
||||
see [up2k](#up2k) for details on how it works
|
||||
@@ -299,11 +318,11 @@ in the `[🚀 up2k]` tab, after toggling the `[🔎]` switch green, any files/fo
|
||||
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
|
||||
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
|
||||
|
||||
adding the same file multiple times is blocked, so if you first search for a file and then decide to upload it, you have to click the `[cleanup]` button to discard `[done]` files
|
||||
adding the same file multiple times is blocked, so if you first search for a file and then decide to upload it, you have to click the `[cleanup]` button to discard `[done]` files (or just refresh the page)
|
||||
|
||||
note that since up2k has to read the file twice, `[🎈 bup]` can be up to 2x faster in extreme cases (if your internet connection is faster than the read-speed of your HDD)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
## markdown viewer
|
||||
@@ -341,11 +360,11 @@ searching relies on two databases, the up2k filetree (`-e2d`) and the metadata t
|
||||
|
||||
through arguments:
|
||||
* `-e2d` enables file indexing on upload
|
||||
* `-e2ds` scans writable folders on startup
|
||||
* `-e2ds` scans writable folders for new files on startup
|
||||
* `-e2dsa` scans all mounted volumes (including readonly ones)
|
||||
* `-e2t` enables metadata indexing on upload
|
||||
* `-e2ts` scans for tags in all files that don't have tags yet
|
||||
* `-e2tsr` deletes all existing tags, so a full reindex
|
||||
* `-e2tsr` deletes all existing tags, does a full reindex
|
||||
|
||||
the same arguments can be set as volume flags, in addition to `d2d` and `d2t` for disabling:
|
||||
* `-v ~/music::r:ce2dsa:ce2tsr` does a full reindex of everything on startup
|
||||
@@ -353,11 +372,11 @@ the same arguments can be set as volume flags, in addition to `d2d` and `d2t` fo
|
||||
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||
|
||||
note:
|
||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those
|
||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
||||
|
||||
you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `cdhash`, this has the following consequences:
|
||||
* initial indexing is way faster, especially when the volume is on a networked disk
|
||||
* initial indexing is way faster, especially when the volume is on a network disk
|
||||
* makes it impossible to [file-search](#file-search)
|
||||
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
||||
|
||||
@@ -390,17 +409,17 @@ 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,)
|
||||
|
||||
`--no-mutagen` disables mutagen and uses ffprobe instead, which...
|
||||
* is about 20x slower than mutagen
|
||||
* catches a few tags that mutagen doesn't
|
||||
`--no-mutagen` disables Mutagen and uses FFprobe instead, which...
|
||||
* is about 20x slower than Mutagen
|
||||
* catches a few tags that Mutagen doesn't
|
||||
* melodic key, video resolution, framerate, pixfmt
|
||||
* 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
|
||||
|
||||
copyparty can invoke external programs to collect additional metadata for files using `mtp` (as argument or volume flag), there is a default timeout of 30sec
|
||||
copyparty can invoke external programs to collect additional metadata for files using `mtp` (either as argument or volume flag), there is a default timeout of 30sec
|
||||
|
||||
* `-mtp .bpm=~/bin/audio-bpm.py` will execute `~/bin/audio-bpm.py` with the audio file as argument 1 to provide the `.bpm` tag, if that does not exist in the audio metadata
|
||||
* `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`)
|
||||
@@ -434,11 +453,13 @@ copyparty can invoke external programs to collect additional metadata for files
|
||||
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| directory tree | - | - | `*1` | yep | yep | yep | yep | yep |
|
||||
| up2k | - | - | yep | yep | yep | yep | yep | yep |
|
||||
| icons work | - | - | yep | yep | yep | yep | yep | yep |
|
||||
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
||||
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
||||
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| play ogg/opus | - | - | - | - | yep | yep | `*2` | yep |
|
||||
| thumbnail view | - | - | - | - | yep | yep | yep | yep |
|
||||
| image viewer | - | - | - | - | yep | yep | yep | yep |
|
||||
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
||||
|
||||
* internet explorer 6 to 8 behave the same
|
||||
* firefox 52 and chrome 49 are the last winxp versions
|
||||
@@ -456,7 +477,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
||||
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
|
||||
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
|
||||
| **ie4** and **netscape** 4.0 | can browse (text is yellow on white), upload with `?b=u` |
|
||||
| **SerenityOS** (22d13d8) | hits a page fault, works with `?b=u`, file input not-impl, url params are multiplying |
|
||||
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
|
||||
|
||||
|
||||
# client examples
|
||||
@@ -510,7 +531,7 @@ below are some tweaks roughly ordered by usefulness:
|
||||
* `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file
|
||||
* `--http-only` or `--https-only` (unless you want to support both protocols) will reduce the delay before a new connection is established
|
||||
* `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set
|
||||
* `--no-hash` when indexing a networked disk if you don't care about the actual filehashes and only want the names/tags searchable
|
||||
* `--no-hash` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable
|
||||
* `-j` enables multiprocessing (actual multithreading) and can make copyparty perform better in cpu-intensive workloads, for example:
|
||||
* huge amount of short-lived connections
|
||||
* really heavy traffic (downloads/uploads)
|
||||
@@ -527,18 +548,18 @@ below are some tweaks roughly ordered by usefulness:
|
||||
|
||||
enable music tags:
|
||||
* 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 image thumbnails:
|
||||
enable thumbnails of images:
|
||||
* `Pillow` (requires py2.7 or py3.5+)
|
||||
|
||||
enable video thumbnails:
|
||||
enable thumbnails of videos:
|
||||
* `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||
|
||||
enable reading HEIF pictures:
|
||||
enable thumbnails of HEIF pictures:
|
||||
* `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||
|
||||
enable reading AVIF pictures:
|
||||
enable thumbnails of AVIF pictures:
|
||||
* `pillow-avif-plugin`
|
||||
|
||||
|
||||
@@ -552,7 +573,7 @@ python -m pip install --user -U jinja2 mutagen Pillow
|
||||
|
||||
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
|
||||
|
||||
these are standalone programs and will never be imported / evaluated by copyparty
|
||||
these are standalone programs and will never be imported / evaluated by copyparty, and must be enabled through `-mtp` configs
|
||||
|
||||
|
||||
# sfx
|
||||
@@ -568,10 +589,10 @@ pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `
|
||||
|
||||
## sfx repack
|
||||
|
||||
if you don't need all the features you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except for either msys2 or WSL if you're on windows)
|
||||
* `724K` original size as of v0.4.0
|
||||
* `256K` after `./scripts/make-sfx.sh re no-ogv`
|
||||
* `164K` after `./scripts/make-sfx.sh re no-ogv no-cm`
|
||||
if you don't need all the features, you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except if you're on windows then you need msys2 or WSL)
|
||||
* `525k` size of original sfx.py as of v0.11.30
|
||||
* `315k` after `./scripts/make-sfx.sh re no-ogv`
|
||||
* `223k` after `./scripts/make-sfx.sh re no-ogv no-cm`
|
||||
|
||||
the features you can opt to drop are
|
||||
* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files
|
||||
@@ -618,7 +639,7 @@ rm -rf copyparty/web/deps
|
||||
curl -L https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py >x.py
|
||||
python3 x.py -h
|
||||
rm x.py
|
||||
mv /tmp/pe-copyparty/copyparty/web/deps/ copyparty/web/
|
||||
mv /tmp/pe-copyparty/copyparty/web/deps/ copyparty/web/deps/
|
||||
```
|
||||
|
||||
then build the sfx using any of the following examples:
|
||||
@@ -646,14 +667,16 @@ in the `scripts` folder:
|
||||
|
||||
roughly sorted by priority
|
||||
|
||||
* hls framework for Someone Else to drop code into :^)
|
||||
* readme.md as epilogue
|
||||
|
||||
|
||||
## discarded ideas
|
||||
|
||||
* reduce up2k roundtrips
|
||||
* start from a chunk index and just go
|
||||
* terminate client on bad data
|
||||
* logging to file
|
||||
|
||||
discarded ideas
|
||||
|
||||
* not worth the effort, just throw enough conncetions at it
|
||||
* single sha512 across all up2k chunks?
|
||||
* crypto.subtle cannot into streaming, would have to use hashwasm, expensive
|
||||
* separate sqlite table per tag
|
||||
|
||||
@@ -345,7 +345,7 @@ class Gateway(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
def sendreq(self, *args, headers={}, **kwargs):
|
||||
def sendreq(self, meth, path, headers, **kwargs):
|
||||
if self.password:
|
||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||
|
||||
@@ -354,21 +354,21 @@ class Gateway(object):
|
||||
if c.rx_path:
|
||||
raise Exception()
|
||||
|
||||
c.request(*list(args), headers=headers, **kwargs)
|
||||
c.request(meth, path, headers=headers, **kwargs)
|
||||
c.rx = c.getresponse()
|
||||
return c
|
||||
except:
|
||||
tid = threading.current_thread().ident
|
||||
dbg(
|
||||
"\033[1;37;44mbad conn {:x}\n {}\n {}\033[0m".format(
|
||||
tid, " ".join(str(x) for x in args), c.rx_path if c else "(null)"
|
||||
"\033[1;37;44mbad conn {:x}\n {} {}\n {}\033[0m".format(
|
||||
tid, meth, path, c.rx_path if c else "(null)"
|
||||
)
|
||||
)
|
||||
|
||||
self.closeconn(c)
|
||||
c = self.getconn()
|
||||
try:
|
||||
c.request(*list(args), headers=headers, **kwargs)
|
||||
c.request(meth, path, headers=headers, **kwargs)
|
||||
c.rx = c.getresponse()
|
||||
return c
|
||||
except:
|
||||
@@ -386,7 +386,7 @@ class Gateway(object):
|
||||
path = dewin(path)
|
||||
|
||||
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
||||
c = self.sendreq("GET", web_path)
|
||||
c = self.sendreq("GET", web_path, {})
|
||||
if c.rx.status != 200:
|
||||
self.closeconn(c)
|
||||
log(
|
||||
@@ -440,7 +440,7 @@ class Gateway(object):
|
||||
)
|
||||
)
|
||||
|
||||
c = self.sendreq("GET", web_path, headers={"Range": hdr_range})
|
||||
c = self.sendreq("GET", web_path, {"Range": hdr_range})
|
||||
if c.rx.status != http.client.PARTIAL_CONTENT:
|
||||
self.closeconn(c)
|
||||
raise Exception(
|
||||
|
||||
@@ -54,10 +54,13 @@ MACOS = platform.system() == "Darwin"
|
||||
info = log = dbg = None
|
||||
|
||||
|
||||
print("{} v{} @ {}".format(
|
||||
platform.python_implementation(),
|
||||
".".join([str(x) for x in sys.version_info]),
|
||||
sys.executable))
|
||||
print(
|
||||
"{} v{} @ {}".format(
|
||||
platform.python_implementation(),
|
||||
".".join([str(x) for x in sys.version_info]),
|
||||
sys.executable,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
@@ -299,14 +302,14 @@ class Gateway(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
def sendreq(self, *args, headers={}, **kwargs):
|
||||
def sendreq(self, meth, path, headers, **kwargs):
|
||||
tid = get_tid()
|
||||
if self.password:
|
||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||
|
||||
try:
|
||||
c = self.getconn(tid)
|
||||
c.request(*list(args), headers=headers, **kwargs)
|
||||
c.request(meth, path, headers=headers, **kwargs)
|
||||
return c.getresponse()
|
||||
except:
|
||||
dbg("bad conn")
|
||||
@@ -314,7 +317,7 @@ class Gateway(object):
|
||||
self.closeconn(tid)
|
||||
try:
|
||||
c = self.getconn(tid)
|
||||
c.request(*list(args), headers=headers, **kwargs)
|
||||
c.request(meth, path, headers=headers, **kwargs)
|
||||
return c.getresponse()
|
||||
except:
|
||||
info("http connection failed:\n" + traceback.format_exc())
|
||||
@@ -331,7 +334,7 @@ class Gateway(object):
|
||||
path = dewin(path)
|
||||
|
||||
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots&ls"
|
||||
r = self.sendreq("GET", web_path)
|
||||
r = self.sendreq("GET", web_path, {})
|
||||
if r.status != 200:
|
||||
self.closeconn()
|
||||
log(
|
||||
@@ -368,7 +371,7 @@ class Gateway(object):
|
||||
)
|
||||
)
|
||||
|
||||
r = self.sendreq("GET", web_path, headers={"Range": hdr_range})
|
||||
r = self.sendreq("GET", web_path, {"Range": hdr_range})
|
||||
if r.status != http.client.PARTIAL_CONTENT:
|
||||
self.closeconn()
|
||||
raise Exception(
|
||||
|
||||
@@ -7,11 +7,19 @@
|
||||
# you may want to:
|
||||
# change '/usr/bin/python' to another interpreter
|
||||
# change '/mnt::a' to another location or permission-set
|
||||
#
|
||||
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
||||
# accept connections; correctly delaying units depending on copyparty.
|
||||
# But note that journalctl will get the timestamps wrong due to
|
||||
# python disabling line-buffering, so messages are out-of-order:
|
||||
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
||||
|
||||
[Unit]
|
||||
Description=copyparty file server
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=copyparty
|
||||
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'
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from textwrap import dedent
|
||||
from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
||||
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
||||
from .svchub import SvcHub
|
||||
from .util import py_desc, align_tab, IMPLICATIONS, alltrace
|
||||
from .util import py_desc, align_tab, IMPLICATIONS
|
||||
|
||||
HAVE_SSL = True
|
||||
try:
|
||||
@@ -191,16 +191,6 @@ def sighandler(sig=None, frame=None):
|
||||
print("\n".join(msg))
|
||||
|
||||
|
||||
def stackmon(fp, ival):
|
||||
ctr = 0
|
||||
while True:
|
||||
ctr += 1
|
||||
time.sleep(ival)
|
||||
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
|
||||
with open(fp, "wb") as f:
|
||||
f.write(st.encode("utf-8", "replace"))
|
||||
|
||||
|
||||
def run_argparse(argv, formatter):
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=formatter,
|
||||
@@ -329,8 +319,9 @@ def run_argparse(argv, formatter):
|
||||
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("--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-ff", action="store_true", help="never use FFprobe as tag reader")
|
||||
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.)",
|
||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,ac,vc,res,.fps")
|
||||
@@ -346,6 +337,7 @@ def run_argparse(argv, formatter):
|
||||
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing")
|
||||
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
||||
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
|
||||
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
|
||||
|
||||
return ap.parse_args(args=argv[1:])
|
||||
# fmt: on
|
||||
@@ -385,16 +377,6 @@ def main(argv=None):
|
||||
except AssertionError:
|
||||
al = run_argparse(argv, Dodge11874)
|
||||
|
||||
if al.stackmon:
|
||||
fp, f = al.stackmon.rsplit(",", 1)
|
||||
f = int(f)
|
||||
t = threading.Thread(
|
||||
target=stackmon,
|
||||
args=(fp, f),
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
# propagate implications
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if getattr(al, k1):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (0, 11, 35)
|
||||
VERSION = (0, 11, 45)
|
||||
CODENAME = "the grid"
|
||||
BUILD_DT = (2021, 7, 11)
|
||||
BUILD_DT = (2021, 7, 20)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -16,7 +16,7 @@ from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir
|
||||
class VFS(object):
|
||||
"""single level in the virtual fs"""
|
||||
|
||||
def __init__(self, log, realpath, vpath, uread=[], uwrite=[], uadm=[], flags={}):
|
||||
def __init__(self, log, realpath, vpath, uread, uwrite, uadm, flags):
|
||||
self.log = log
|
||||
self.realpath = realpath # absolute path on host filesystem
|
||||
self.vpath = vpath # absolute path in the virtual filesystem
|
||||
@@ -81,7 +81,7 @@ class VFS(object):
|
||||
|
||||
# leaf does not exist; create and keep permissions blank
|
||||
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
||||
vn = VFS(self.log, src, vp)
|
||||
vn = VFS(self.log, src, vp, [], [], [], {})
|
||||
vn.dbv = self.dbv or self
|
||||
self.nodes[dst] = vn
|
||||
return vn
|
||||
@@ -497,10 +497,10 @@ class AuthSrv(object):
|
||||
|
||||
if not mount:
|
||||
# -h says our defaults are CWD at root and read/write for everyone
|
||||
vfs = VFS(self.log_func, os.path.abspath("."), "", ["*"], ["*"])
|
||||
vfs = VFS(self.log_func, os.path.abspath("."), "", ["*"], ["*"], ["*"], {})
|
||||
elif "" not in mount:
|
||||
# there's volumes but no root; make root inaccessible
|
||||
vfs = VFS(self.log_func, None, "")
|
||||
vfs = VFS(self.log_func, None, "", [], [], [], {})
|
||||
vfs.flags["d2d"] = True
|
||||
|
||||
maxdepth = 0
|
||||
|
||||
@@ -27,18 +27,17 @@ class BrokerMp(object):
|
||||
cores = mp.cpu_count()
|
||||
|
||||
self.log("broker", "booting {} subprocesses".format(cores))
|
||||
for n in range(cores):
|
||||
for n in range(1, cores + 1):
|
||||
q_pend = mp.Queue(1)
|
||||
q_yield = mp.Queue(64)
|
||||
|
||||
proc = mp.Process(target=MpWorker, args=(q_pend, q_yield, self.args, n))
|
||||
proc.q_pend = q_pend
|
||||
proc.q_yield = q_yield
|
||||
proc.nid = n
|
||||
proc.clients = {}
|
||||
|
||||
thr = threading.Thread(
|
||||
target=self.collector, args=(proc,), name="mp-collector"
|
||||
target=self.collector, args=(proc,), name="mp-sink-{}".format(n)
|
||||
)
|
||||
thr.daemon = True
|
||||
thr.start()
|
||||
|
||||
@@ -29,13 +29,14 @@ class MpWorker(object):
|
||||
# we inherited signal_handler from parent,
|
||||
# replace it with something harmless
|
||||
if not FAKE_MP:
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||
signal.signal(sig, self.signal_handler)
|
||||
|
||||
# starting to look like a good idea
|
||||
self.asrv = AuthSrv(args, None, False)
|
||||
|
||||
# instantiate all services here (TODO: inheritance?)
|
||||
self.httpsrv = HttpSrv(self, True)
|
||||
self.httpsrv = HttpSrv(self, n)
|
||||
|
||||
# on winxp and some other platforms,
|
||||
# use thr.join() to block all signals
|
||||
@@ -44,7 +45,7 @@ class MpWorker(object):
|
||||
thr.start()
|
||||
thr.join()
|
||||
|
||||
def signal_handler(self, signal, frame):
|
||||
def signal_handler(self, sig, frame):
|
||||
# print('k')
|
||||
pass
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class BrokerThr(object):
|
||||
self.mutex = threading.Lock()
|
||||
|
||||
# instantiate all services here (TODO: inheritance?)
|
||||
self.httpsrv = HttpSrv(self)
|
||||
self.httpsrv = HttpSrv(self, None)
|
||||
|
||||
def shutdown(self):
|
||||
# self.log("broker", "shutting down")
|
||||
|
||||
@@ -37,7 +37,6 @@ class HttpCli(object):
|
||||
self.ip = conn.addr[0]
|
||||
self.addr = conn.addr # type: tuple[str, int]
|
||||
self.args = conn.args
|
||||
self.is_mp = conn.is_mp
|
||||
self.asrv = conn.asrv # type: AuthSrv
|
||||
self.ico = conn.ico
|
||||
self.thumbcli = conn.thumbcli
|
||||
@@ -227,7 +226,7 @@ class HttpCli(object):
|
||||
except Pebkac:
|
||||
return False
|
||||
|
||||
def send_headers(self, length, status=200, mime=None, headers={}):
|
||||
def send_headers(self, length, status=200, mime=None, headers=None):
|
||||
response = ["{} {} {}".format(self.http_ver, status, HTTPCODE[status])]
|
||||
|
||||
if length is not None:
|
||||
@@ -237,7 +236,8 @@ class HttpCli(object):
|
||||
response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close"))
|
||||
|
||||
# headers{} overrides anything set previously
|
||||
self.out_headers.update(headers)
|
||||
if headers:
|
||||
self.out_headers.update(headers)
|
||||
|
||||
# default to utf8 html if no content-type is set
|
||||
if not mime:
|
||||
@@ -254,7 +254,7 @@ class HttpCli(object):
|
||||
except:
|
||||
raise Pebkac(400, "client d/c while replying headers")
|
||||
|
||||
def reply(self, body, status=200, mime=None, headers={}):
|
||||
def reply(self, body, status=200, mime=None, headers=None):
|
||||
# TODO something to reply with user-supplied values safely
|
||||
self.send_headers(len(body), status, mime, headers)
|
||||
|
||||
@@ -270,7 +270,7 @@ class HttpCli(object):
|
||||
self.log(body.rstrip())
|
||||
self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
|
||||
|
||||
def urlq(self, add={}, rm=[]):
|
||||
def urlq(self, add, rm):
|
||||
"""
|
||||
generates url query based on uparam (b, pw, all others)
|
||||
removing anything in rm, adding pairs in add
|
||||
@@ -342,6 +342,9 @@ class HttpCli(object):
|
||||
if "tree" in self.uparam:
|
||||
return self.tx_tree()
|
||||
|
||||
if "stack" in self.uparam:
|
||||
return self.tx_stack()
|
||||
|
||||
# conditional redirect to single volumes
|
||||
if self.vpath == "" and not self.ouparam:
|
||||
nread = len(self.rvol)
|
||||
@@ -371,9 +374,6 @@ class HttpCli(object):
|
||||
if "scan" in self.uparam:
|
||||
return self.scanvol()
|
||||
|
||||
if "stack" in self.uparam:
|
||||
return self.tx_stack()
|
||||
|
||||
return self.tx_browser()
|
||||
|
||||
def handle_options(self):
|
||||
@@ -795,7 +795,7 @@ class HttpCli(object):
|
||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
sanitized = sanitize_fn(new_dir)
|
||||
sanitized = sanitize_fn(new_dir, "", [])
|
||||
|
||||
if not nullwrite:
|
||||
fdir = os.path.join(vfs.realpath, rem)
|
||||
@@ -832,7 +832,7 @@ class HttpCli(object):
|
||||
if not new_file.endswith(".md"):
|
||||
new_file += ".md"
|
||||
|
||||
sanitized = sanitize_fn(new_file)
|
||||
sanitized = sanitize_fn(new_file, "", [])
|
||||
|
||||
if not nullwrite:
|
||||
fdir = os.path.join(vfs.realpath, rem)
|
||||
@@ -865,7 +865,7 @@ class HttpCli(object):
|
||||
if p_file and not nullwrite:
|
||||
fdir = os.path.join(vfs.realpath, rem)
|
||||
fname = sanitize_fn(
|
||||
p_file, bad=[".prologue.html", ".epilogue.html"]
|
||||
p_file, "", [".prologue.html", ".epilogue.html"]
|
||||
)
|
||||
|
||||
if not os.path.isdir(fsenc(fdir)):
|
||||
@@ -1312,7 +1312,7 @@ class HttpCli(object):
|
||||
|
||||
fgen = vn.zipgen(rem, items, self.uname, dots, not self.args.no_scandir)
|
||||
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
|
||||
bgen = packer(fgen, utf8="utf" in uarg, pre_crc="crc" in uarg)
|
||||
bgen = packer(self.log, fgen, utf8="utf" in uarg, pre_crc="crc" in uarg)
|
||||
bsent = 0
|
||||
for buf in bgen.gen():
|
||||
if not buf:
|
||||
@@ -1376,7 +1376,7 @@ class HttpCli(object):
|
||||
for c, v in [[b"&", 4], [b"<", 3], [b">", 3]]:
|
||||
sz_md += (len(buf) - len(buf.replace(c, b""))) * v
|
||||
|
||||
file_ts = max(ts_md, ts_html)
|
||||
file_ts = max(ts_md, ts_html, E.t0)
|
||||
file_lastmod, do_send = self._chk_lastmod(file_ts)
|
||||
self.out_headers["Last-Modified"] = file_lastmod
|
||||
self.out_headers.update(NO_CACHE)
|
||||
@@ -1423,7 +1423,7 @@ class HttpCli(object):
|
||||
return True
|
||||
|
||||
def tx_mounts(self):
|
||||
suf = self.urlq(rm=["h"])
|
||||
suf = self.urlq({}, ["h"])
|
||||
rvol, wvol, avol = [
|
||||
[("/" + x).rstrip("/") + "/" for x in y]
|
||||
for y in [self.rvol, self.wvol, self.avol]
|
||||
@@ -1634,7 +1634,7 @@ class HttpCli(object):
|
||||
if self.writable:
|
||||
perms.append("write")
|
||||
|
||||
url_suf = self.urlq()
|
||||
url_suf = self.urlq({}, [])
|
||||
is_ls = "ls" in self.uparam
|
||||
|
||||
tpl = "browser"
|
||||
@@ -1653,6 +1653,7 @@ class HttpCli(object):
|
||||
"files": [],
|
||||
"taglist": [],
|
||||
"srvinf": srv_info,
|
||||
"acct": self.uname,
|
||||
"perms": perms,
|
||||
"logues": logues,
|
||||
}
|
||||
@@ -1660,6 +1661,7 @@ class HttpCli(object):
|
||||
"vdir": quotep(self.vpath),
|
||||
"vpnodes": vpnodes,
|
||||
"files": [],
|
||||
"acct": self.uname,
|
||||
"perms": json.dumps(perms),
|
||||
"taglist": [],
|
||||
"tag_order": [],
|
||||
|
||||
@@ -34,7 +34,6 @@ class HttpConn(object):
|
||||
|
||||
self.args = hsrv.args
|
||||
self.asrv = hsrv.asrv
|
||||
self.is_mp = hsrv.is_mp
|
||||
self.cert_path = hsrv.cert_path
|
||||
|
||||
enth = HAVE_PIL and not self.args.no_thumb
|
||||
|
||||
@@ -27,7 +27,7 @@ except ImportError:
|
||||
sys.exit(1)
|
||||
|
||||
from .__init__ import E, PY2, MACOS
|
||||
from .util import spack, min_ex
|
||||
from .util import spack, min_ex, start_stackmon, start_log_thrs
|
||||
from .httpconn import HttpConn
|
||||
|
||||
if PY2:
|
||||
@@ -42,14 +42,14 @@ class HttpSrv(object):
|
||||
relying on MpSrv for performance (HttpSrv is just plain threads)
|
||||
"""
|
||||
|
||||
def __init__(self, broker, is_mp=False):
|
||||
def __init__(self, broker, nid):
|
||||
self.broker = broker
|
||||
self.is_mp = is_mp
|
||||
self.nid = nid
|
||||
self.args = broker.args
|
||||
self.log = broker.log
|
||||
self.asrv = broker.asrv
|
||||
|
||||
self.name = "httpsrv-i{:x}".format(os.getpid())
|
||||
self.name = "httpsrv" + ("-n{}-i{:x}".format(nid, os.getpid()) if nid else "")
|
||||
self.mutex = threading.Lock()
|
||||
self.stopping = False
|
||||
|
||||
@@ -81,10 +81,18 @@ class HttpSrv(object):
|
||||
if self.tp_q:
|
||||
self.start_threads(4)
|
||||
|
||||
t = threading.Thread(target=self.thr_scaler)
|
||||
name = "httpsrv-scaler" + ("-{}".format(nid) if nid else "")
|
||||
t = threading.Thread(target=self.thr_scaler, name=name)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
if nid:
|
||||
if self.args.stackmon:
|
||||
start_stackmon(self.args.stackmon, nid)
|
||||
|
||||
if self.args.log_thrs:
|
||||
start_log_thrs(self.log, self.args.log_thrs, nid)
|
||||
|
||||
def start_threads(self, n):
|
||||
self.tp_nthr += n
|
||||
if self.args.log_htp:
|
||||
@@ -93,7 +101,7 @@ class HttpSrv(object):
|
||||
for _ in range(n):
|
||||
thr = threading.Thread(
|
||||
target=self.thr_poolw,
|
||||
name="httpsrv-poolw",
|
||||
name=self.name + "-poolw",
|
||||
)
|
||||
thr.daemon = True
|
||||
thr.start()
|
||||
@@ -115,9 +123,14 @@ class HttpSrv(object):
|
||||
self.stop_threads(4)
|
||||
|
||||
def listen(self, sck, nlisteners):
|
||||
ip, port = sck.getsockname()
|
||||
self.srvs.append(sck)
|
||||
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
||||
t = threading.Thread(target=self.thr_listen, args=(sck,))
|
||||
t = threading.Thread(
|
||||
target=self.thr_listen,
|
||||
args=(sck,),
|
||||
name="httpsrv-n{}-listen-{}-{}".format(self.nid or "0", ip, port),
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
@@ -158,7 +171,7 @@ class HttpSrv(object):
|
||||
"""takes an incoming tcp connection and creates a thread to handle it"""
|
||||
now = time.time()
|
||||
|
||||
if self.tp_time and now - self.tp_time > 300:
|
||||
if now - (self.tp_time or now) > 300:
|
||||
self.tp_q = None
|
||||
|
||||
if self.tp_q:
|
||||
@@ -181,7 +194,7 @@ class HttpSrv(object):
|
||||
thr = threading.Thread(
|
||||
target=self.thr_client,
|
||||
args=(sck, addr),
|
||||
name="httpsrv-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
||||
name="httpconn-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
||||
)
|
||||
thr.daemon = True
|
||||
thr.start()
|
||||
@@ -198,11 +211,11 @@ class HttpSrv(object):
|
||||
try:
|
||||
sck, addr = task
|
||||
me = threading.current_thread()
|
||||
me.name = (
|
||||
"httpsrv-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
||||
me.name = "httpconn-{}-{}".format(
|
||||
addr[0].split(".", 2)[-1][-6:], addr[1]
|
||||
)
|
||||
self.thr_client(sck, addr)
|
||||
me.name = "httpsrv-poolw"
|
||||
me.name = self.name + "-poolw"
|
||||
except:
|
||||
self.log(self.name, "thr_client: " + min_ex(), 3)
|
||||
|
||||
@@ -228,7 +241,7 @@ class HttpSrv(object):
|
||||
if self.tp_q.empty():
|
||||
break
|
||||
|
||||
self.log("httpsrv-i" + str(os.getpid()), "ok bye")
|
||||
self.log(self.name, "ok bye")
|
||||
|
||||
def thr_client(self, sck, addr):
|
||||
"""thread managing one tcp client"""
|
||||
|
||||
@@ -227,37 +227,47 @@ def parse_ffprobe(txt):
|
||||
class MTag(object):
|
||||
def __init__(self, log_func, args):
|
||||
self.log_func = log_func
|
||||
self.args = args
|
||||
self.usable = True
|
||||
self.prefer_mt = False
|
||||
mappings = args.mtm
|
||||
self.prefer_mt = not args.no_mtag_ff
|
||||
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":
|
||||
self.get = self.get_mutagen
|
||||
try:
|
||||
import mutagen
|
||||
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"
|
||||
|
||||
if self.backend == "ffprobe":
|
||||
self.usable = self.can_ffprobe
|
||||
self.get = self.get_ffprobe
|
||||
self.prefer_mt = True
|
||||
# about 20x slower
|
||||
self.usable = HAVE_FFPROBE
|
||||
|
||||
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
||||
self.usable = False
|
||||
if not HAVE_FFPROBE:
|
||||
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"
|
||||
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)
|
||||
|
||||
if not self.usable:
|
||||
msg = "need mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
||||
self.log(
|
||||
msg.format(or_ffprobe, " " * 37, os.path.basename(sys.executable)), c=1
|
||||
)
|
||||
msg = "need Mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
||||
pybin = os.path.basename(sys.executable)
|
||||
self.log(msg.format(or_ffprobe, " " * 37, pybin), c=1)
|
||||
return
|
||||
|
||||
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||
@@ -387,7 +397,7 @@ class MTag(object):
|
||||
v2 = r2.get(k)
|
||||
if v1 == v2:
|
||||
print(" ", k, v1)
|
||||
elif v1 != "0000": # ffprobe date=0
|
||||
elif v1 != "0000": # FFprobe date=0
|
||||
diffs.append(k)
|
||||
print(" 1", k, v1)
|
||||
print(" 2", k, v2)
|
||||
@@ -408,20 +418,33 @@ class MTag(object):
|
||||
md = mutagen.File(fsenc(abspath), easy=True)
|
||||
x = md.info.length
|
||||
except Exception as ex:
|
||||
return {}
|
||||
return self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
||||
|
||||
ret = {}
|
||||
try:
|
||||
dur = int(md.info.length)
|
||||
sz = os.path.getsize(fsenc(abspath))
|
||||
ret = {".q": [0, int((sz / md.info.length) / 128)]}
|
||||
|
||||
for attr, k, norm in [
|
||||
["codec", "ac", unicode],
|
||||
["channels", "chs", int],
|
||||
["sample_rate", ".hz", int],
|
||||
["bitrate", ".aq", int],
|
||||
["length", ".dur", int],
|
||||
]:
|
||||
try:
|
||||
q = int(md.info.bitrate / 1024)
|
||||
v = getattr(md.info, attr)
|
||||
except:
|
||||
q = int((os.path.getsize(fsenc(abspath)) / dur) / 128)
|
||||
continue
|
||||
|
||||
ret[".dur"] = [0, dur]
|
||||
ret[".q"] = [0, q]
|
||||
except:
|
||||
pass
|
||||
if not v:
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -33,10 +33,11 @@ class QFile(object):
|
||||
class StreamTar(object):
|
||||
"""construct in-memory tar file from the given path"""
|
||||
|
||||
def __init__(self, fgen, **kwargs):
|
||||
def __init__(self, log, fgen, **kwargs):
|
||||
self.ci = 0
|
||||
self.co = 0
|
||||
self.qfile = QFile()
|
||||
self.log = log
|
||||
self.fgen = fgen
|
||||
self.errf = None
|
||||
|
||||
@@ -91,7 +92,8 @@ class StreamTar(object):
|
||||
errors.append([f["vp"], repr(ex)])
|
||||
|
||||
if errors:
|
||||
self.errf = errdesc(errors)
|
||||
self.errf, txt = errdesc(errors)
|
||||
self.log("\n".join(([repr(self.errf)] + txt[1:])))
|
||||
self.ser(self.errf)
|
||||
|
||||
self.tar.close()
|
||||
|
||||
@@ -25,4 +25,4 @@ def errdesc(errors):
|
||||
"vp": "archive-errors-{}.txt".format(dt),
|
||||
"ap": tf_path,
|
||||
"st": os.stat(tf_path),
|
||||
}
|
||||
}, report
|
||||
|
||||
@@ -6,12 +6,15 @@ import os
|
||||
import sys
|
||||
import time
|
||||
import shlex
|
||||
import string
|
||||
import signal
|
||||
import socket
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
import calendar
|
||||
|
||||
from .__init__ import E, PY2, WINDOWS, MACOS, VT100
|
||||
from .util import mp
|
||||
from .__init__ import E, PY2, WINDOWS, MACOS, VT100, unicode
|
||||
from .util import mp, start_log_thrs, start_stackmon, min_ex
|
||||
from .authsrv import AuthSrv
|
||||
from .tcpsrv import TcpSrv
|
||||
from .up2k import Up2k
|
||||
@@ -33,6 +36,9 @@ class SvcHub(object):
|
||||
self.args = args
|
||||
self.argv = argv
|
||||
self.logf = None
|
||||
self.stop_req = False
|
||||
self.stopping = False
|
||||
self.stop_cond = threading.Condition()
|
||||
|
||||
self.ansi_re = re.compile("\033\\[[^m]*m")
|
||||
self.log_mutex = threading.Lock()
|
||||
@@ -42,6 +48,12 @@ class SvcHub(object):
|
||||
if args.lo:
|
||||
self._setup_logfile(printed)
|
||||
|
||||
if args.stackmon:
|
||||
start_stackmon(args.stackmon, 0)
|
||||
|
||||
if args.log_thrs:
|
||||
start_log_thrs(self.log, args.log_thrs, 0)
|
||||
|
||||
# initiate all services to manage
|
||||
self.asrv = AuthSrv(self.args, self.log, False)
|
||||
if args.ls:
|
||||
@@ -121,21 +133,55 @@ class SvcHub(object):
|
||||
print(msg, end="")
|
||||
|
||||
def run(self):
|
||||
thr = threading.Thread(target=self.tcpsrv.run, name="svchub-main")
|
||||
self.tcpsrv.run()
|
||||
|
||||
thr = threading.Thread(target=self.sd_notify, name="sd-notify")
|
||||
thr.daemon = True
|
||||
thr.start()
|
||||
|
||||
# winxp/py2.7 support: thr.join() kills signals
|
||||
try:
|
||||
while True:
|
||||
time.sleep(9001)
|
||||
thr = threading.Thread(target=self.stop_thr, name="svchub-sig")
|
||||
thr.daemon = True
|
||||
thr.start()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||
signal.signal(sig, self.signal_handler)
|
||||
|
||||
try:
|
||||
while not self.stop_req:
|
||||
time.sleep(9001)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.shutdown()
|
||||
|
||||
def stop_thr(self):
|
||||
while not self.stop_req:
|
||||
with self.stop_cond:
|
||||
self.stop_cond.wait(9001)
|
||||
|
||||
self.shutdown()
|
||||
|
||||
def signal_handler(self):
|
||||
if self.stopping:
|
||||
return
|
||||
|
||||
self.stop_req = True
|
||||
with self.stop_cond:
|
||||
self.stop_cond.notify_all()
|
||||
|
||||
def shutdown(self):
|
||||
if self.stopping:
|
||||
return
|
||||
|
||||
self.stopping = True
|
||||
self.stop_req = True
|
||||
try:
|
||||
with self.log_mutex:
|
||||
print("OPYTHAT")
|
||||
|
||||
self.tcpsrv.shutdown()
|
||||
self.broker.shutdown()
|
||||
self.up2k.shutdown()
|
||||
if self.thumbsrv:
|
||||
self.thumbsrv.shutdown()
|
||||
|
||||
@@ -262,3 +308,22 @@ class SvcHub(object):
|
||||
else:
|
||||
self.log("svchub", err)
|
||||
return False
|
||||
|
||||
def sd_notify(self):
|
||||
try:
|
||||
addr = os.getenv("NOTIFY_SOCKET")
|
||||
if not addr:
|
||||
return
|
||||
|
||||
addr = unicode(addr)
|
||||
if addr.startswith("@"):
|
||||
addr = "\0" + addr[1:]
|
||||
|
||||
m = "".join(x for x in addr if x in string.printable)
|
||||
self.log("sd_notify", m)
|
||||
|
||||
sck = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
sck.connect(addr)
|
||||
sck.sendall(b"READY=1")
|
||||
except:
|
||||
self.log("sd_notify", min_ex())
|
||||
|
||||
@@ -89,7 +89,7 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
|
||||
ret += spack(b"<LL", vsz, vsz)
|
||||
|
||||
# windows support (the "?" replace below too)
|
||||
fn = sanitize_fn(fn, ok="/")
|
||||
fn = sanitize_fn(fn, "/", [])
|
||||
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
||||
|
||||
z64_len = len(z64v) * 8 + 4 if z64v else 0
|
||||
@@ -183,7 +183,8 @@ def gen_ecdr64_loc(ecdr64_pos):
|
||||
|
||||
|
||||
class StreamZip(object):
|
||||
def __init__(self, fgen, utf8=False, pre_crc=False):
|
||||
def __init__(self, log, fgen, utf8=False, pre_crc=False):
|
||||
self.log = log
|
||||
self.fgen = fgen
|
||||
self.utf8 = utf8
|
||||
self.pre_crc = pre_crc
|
||||
@@ -246,8 +247,8 @@ class StreamZip(object):
|
||||
errors.append([f["vp"], repr(ex)])
|
||||
|
||||
if errors:
|
||||
errf = errdesc(errors)
|
||||
print(repr(errf))
|
||||
errf, txt = errdesc(errors)
|
||||
self.log("\n".join(([repr(errf)] + txt[1:])))
|
||||
for x in self.ser(errf):
|
||||
yield x
|
||||
|
||||
|
||||
@@ -121,10 +121,10 @@ class ThumbSrv(object):
|
||||
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
||||
missing = []
|
||||
if not HAVE_FFMPEG:
|
||||
missing.append("ffmpeg")
|
||||
missing.append("FFmpeg")
|
||||
|
||||
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 += ", ".join(missing)
|
||||
@@ -260,7 +260,7 @@ class ThumbSrv(object):
|
||||
pass # default q = 75
|
||||
|
||||
if im.mode not in fmts:
|
||||
print("conv {}".format(im.mode))
|
||||
# print("conv {}".format(im.mode))
|
||||
im = im.convert("RGB")
|
||||
|
||||
im.save(tpath, quality=40, method=6)
|
||||
|
||||
@@ -7,6 +7,7 @@ import time
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
from .__init__ import unicode
|
||||
from .util import s3dec, Pebkac, min_ex
|
||||
from .up2k import up2k_wark_from_hashlist
|
||||
|
||||
@@ -90,6 +91,8 @@ class U2idx(object):
|
||||
mt_ctr = 0
|
||||
mt_keycmp = "substr(up.w,1,16)"
|
||||
mt_keycmp2 = None
|
||||
ptn_lc = re.compile(r" (mt[0-9]+\.v) ([=<!>]+) \? $")
|
||||
ptn_lcv = re.compile(r"[a-zA-Z]")
|
||||
|
||||
while True:
|
||||
uq = uq.strip()
|
||||
@@ -182,6 +185,21 @@ class U2idx(object):
|
||||
va.append(v)
|
||||
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:
|
||||
return self.run_query(vols, joins + "where " + q, va)
|
||||
except Exception as ex:
|
||||
|
||||
@@ -195,7 +195,7 @@ class Up2k(object):
|
||||
|
||||
return True, ret
|
||||
|
||||
def init_indexes(self, all_vols, scan_vols=[]):
|
||||
def init_indexes(self, all_vols, scan_vols=None):
|
||||
self.pp = ProgressPrinter()
|
||||
vols = all_vols.values()
|
||||
t0 = time.time()
|
||||
@@ -304,7 +304,7 @@ class Up2k(object):
|
||||
self.log(msg.format(len(vols), time.time() - t0))
|
||||
|
||||
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)
|
||||
|
||||
thr = None
|
||||
@@ -596,7 +596,7 @@ class Up2k(object):
|
||||
c2 = conn.cursor()
|
||||
c3 = conn.cursor()
|
||||
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
|
||||
q = "select w from mt where w = ?"
|
||||
if c2.execute(q, (w[:16],)).fetchone():
|
||||
@@ -991,7 +991,7 @@ class Up2k(object):
|
||||
if cj["ptop"] not in self.registry:
|
||||
raise Pebkac(410, "location unavailable")
|
||||
|
||||
cj["name"] = sanitize_fn(cj["name"], bad=[".prologue.html", ".epilogue.html"])
|
||||
cj["name"] = sanitize_fn(cj["name"], "", [".prologue.html", ".epilogue.html"])
|
||||
cj["poke"] = time.time()
|
||||
wark = self._get_wark(cj)
|
||||
now = time.time()
|
||||
@@ -1363,19 +1363,22 @@ class Up2k(object):
|
||||
self.log("could not unsparse [{}]".format(path), 3)
|
||||
|
||||
def _snapshot(self):
|
||||
persist_interval = 30 # persist unfinished uploads index every 30 sec
|
||||
discard_interval = 21600 # drop unfinished uploads after 6 hours inactivity
|
||||
prev = {}
|
||||
self.snap_persist_interval = 300 # persist unfinished index every 5 min
|
||||
self.snap_discard_interval = 21600 # drop unfinished after 6 hours inactivity
|
||||
self.snap_prev = {}
|
||||
while True:
|
||||
time.sleep(persist_interval)
|
||||
with self.mutex:
|
||||
for k, reg in self.registry.items():
|
||||
self._snap_reg(prev, k, reg, discard_interval)
|
||||
time.sleep(self.snap_persist_interval)
|
||||
self.do_snapshot()
|
||||
|
||||
def _snap_reg(self, prev, ptop, reg, discard_interval):
|
||||
def do_snapshot(self):
|
||||
with self.mutex:
|
||||
for k, reg in self.registry.items():
|
||||
self._snap_reg(k, reg)
|
||||
|
||||
def _snap_reg(self, ptop, reg):
|
||||
now = time.time()
|
||||
histpath = self.asrv.vfs.histtab[ptop]
|
||||
rm = [x for x in reg.values() if now - x["poke"] > discard_interval]
|
||||
rm = [x for x in reg.values() if now - x["poke"] > self.snap_discard_interval]
|
||||
if rm:
|
||||
m = "dropping {} abandoned uploads in {}".format(len(rm), ptop)
|
||||
vis = [self._vis_job_progress(x) for x in rm]
|
||||
@@ -1397,15 +1400,15 @@ class Up2k(object):
|
||||
|
||||
path = os.path.join(histpath, "up2k.snap")
|
||||
if not reg:
|
||||
if ptop not in prev or prev[ptop] is not None:
|
||||
prev[ptop] = None
|
||||
if ptop not in self.snap_prev or self.snap_prev[ptop] is not None:
|
||||
self.snap_prev[ptop] = None
|
||||
if os.path.exists(fsenc(path)):
|
||||
os.unlink(fsenc(path))
|
||||
return
|
||||
|
||||
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
||||
etag = [len(reg), newest]
|
||||
if etag == prev.get(ptop):
|
||||
if etag == self.snap_prev.get(ptop):
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -1421,7 +1424,7 @@ class Up2k(object):
|
||||
atomic_move(path2, path)
|
||||
|
||||
self.log("snap: {} |{}|".format(path, len(reg.keys())))
|
||||
prev[ptop] = etag
|
||||
self.snap_prev[ptop] = etag
|
||||
|
||||
def _tagger(self):
|
||||
with self.mutex:
|
||||
@@ -1489,6 +1492,11 @@ class Up2k(object):
|
||||
self.n_hashq += 1
|
||||
# self.log("hashq {} push {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
||||
|
||||
def shutdown(self):
|
||||
if hasattr(self, "snap_prev"):
|
||||
self.log("writing snapshot")
|
||||
self.do_snapshot()
|
||||
|
||||
|
||||
def up2k_chunksize(filesize):
|
||||
chunksize = 1024 * 1024
|
||||
@@ -1504,7 +1512,7 @@ def up2k_chunksize(filesize):
|
||||
|
||||
|
||||
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.extend(hashes)
|
||||
ident = "\n".join(ident)
|
||||
|
||||
@@ -16,6 +16,7 @@ import mimetypes
|
||||
import contextlib
|
||||
import subprocess as sp # nosec
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
|
||||
from .__init__ import PY2, WINDOWS, ANYWIN
|
||||
from .stolen import surrogateescape
|
||||
@@ -282,6 +283,62 @@ def alltrace():
|
||||
return "\n".join(rret + bret)
|
||||
|
||||
|
||||
def start_stackmon(arg_str, nid):
|
||||
suffix = "-{}".format(nid) if nid else ""
|
||||
fp, f = arg_str.rsplit(",", 1)
|
||||
f = int(f)
|
||||
t = threading.Thread(
|
||||
target=stackmon,
|
||||
args=(fp, f, suffix),
|
||||
name="stackmon" + suffix,
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
def stackmon(fp, ival, suffix):
|
||||
ctr = 0
|
||||
while True:
|
||||
ctr += 1
|
||||
time.sleep(ival)
|
||||
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
|
||||
with open(fp + suffix, "wb") as f:
|
||||
f.write(st.encode("utf-8", "replace"))
|
||||
|
||||
|
||||
def start_log_thrs(logger, ival, nid):
|
||||
ival = int(ival)
|
||||
tname = lname = "log-thrs"
|
||||
if nid:
|
||||
tname = "logthr-n{}-i{:x}".format(nid, os.getpid())
|
||||
lname = tname[3:]
|
||||
|
||||
t = threading.Thread(
|
||||
target=log_thrs,
|
||||
args=(logger, ival, lname),
|
||||
name=tname,
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
def log_thrs(log, ival, name):
|
||||
while True:
|
||||
time.sleep(ival)
|
||||
tv = [x.name for x in threading.enumerate()]
|
||||
tv = [
|
||||
x.split("-")[0]
|
||||
if x.startswith("httpconn-") or x.startswith("thumb-")
|
||||
else "listen"
|
||||
if "-listen-" in x
|
||||
else x
|
||||
for x in tv
|
||||
if not x.startswith("pydevd.")
|
||||
]
|
||||
tv = ["{}\033[36m{}".format(v, k) for k, v in sorted(Counter(tv).items())]
|
||||
log(name, "\033[0m \033[33m".join(tv), 3)
|
||||
|
||||
|
||||
def min_ex():
|
||||
et, ev, tb = sys.exc_info()
|
||||
tb = traceback.extract_tb(tb)
|
||||
@@ -672,7 +729,7 @@ def undot(path):
|
||||
return "/".join(ret)
|
||||
|
||||
|
||||
def sanitize_fn(fn, ok="", bad=[]):
|
||||
def sanitize_fn(fn, ok, bad):
|
||||
if "/" not in ok:
|
||||
fn = fn.replace("\\", "/").split("/")[-1]
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ window.baguetteBox = (function () {
|
||||
captions: true,
|
||||
buttons: 'auto',
|
||||
noScrollbars: false,
|
||||
bodyClass: 'baguetteBox-open',
|
||||
bodyClass: 'bbox-open',
|
||||
titleTag: false,
|
||||
async: false,
|
||||
preload: 2,
|
||||
@@ -22,38 +22,46 @@ window.baguetteBox = (function () {
|
||||
afterHide: null,
|
||||
onChange: null,
|
||||
},
|
||||
overlay, slider, previousButton, nextButton, closeButton,
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnVmode, btnClose,
|
||||
currentGallery = [],
|
||||
currentIndex = 0,
|
||||
isOverlayVisible = false,
|
||||
touch = {}, // start-pos
|
||||
touchFlag = false, // busy
|
||||
re_i = /.+\.(gif|jpe?g|png|webp)/i,
|
||||
re_v = /.+\.(webm|mp4)/i,
|
||||
re_i = /.+\.(gif|jpe?g|png|webp)(\?|$)/i,
|
||||
re_v = /.+\.(webm|mp4)(\?|$)/i,
|
||||
data = {}, // all galleries
|
||||
imagesElements = [],
|
||||
documentLastFocus = null;
|
||||
documentLastFocus = null,
|
||||
isFullscreen = false,
|
||||
vmute = false,
|
||||
vloop = false,
|
||||
vnext = false,
|
||||
resume_mp = false;
|
||||
|
||||
var overlayClickHandler = function (event) {
|
||||
if (event.target.id.indexOf('baguette-img') !== -1) {
|
||||
var onFSC = function (e) {
|
||||
isFullscreen = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
var overlayClickHandler = function (e) {
|
||||
if (e.target.id.indexOf('baguette-img') !== -1)
|
||||
hideOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
var touchstartHandler = function (event) {
|
||||
var touchstartHandler = function (e) {
|
||||
touch.count++;
|
||||
if (touch.count > 1) {
|
||||
if (touch.count > 1)
|
||||
touch.multitouch = true;
|
||||
}
|
||||
touch.startX = event.changedTouches[0].pageX;
|
||||
touch.startY = event.changedTouches[0].pageY;
|
||||
|
||||
touch.startX = e.changedTouches[0].pageX;
|
||||
touch.startY = e.changedTouches[0].pageY;
|
||||
};
|
||||
var touchmoveHandler = function (event) {
|
||||
if (touchFlag || touch.multitouch) {
|
||||
var touchmoveHandler = function (e) {
|
||||
if (touchFlag || touch.multitouch)
|
||||
return;
|
||||
}
|
||||
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
||||
var touchEvent = event.touches[0] || event.changedTouches[0];
|
||||
|
||||
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||
var touchEvent = e.touches[0] || e.changedTouches[0];
|
||||
if (touchEvent.pageX - touch.startX > 40) {
|
||||
touchFlag = true;
|
||||
showPreviousImage();
|
||||
@@ -66,19 +74,19 @@ window.baguetteBox = (function () {
|
||||
};
|
||||
var touchendHandler = function () {
|
||||
touch.count--;
|
||||
if (touch.count <= 0) {
|
||||
if (touch.count <= 0)
|
||||
touch.multitouch = false;
|
||||
}
|
||||
|
||||
touchFlag = false;
|
||||
};
|
||||
var contextmenuHandler = function () {
|
||||
touchendHandler();
|
||||
};
|
||||
|
||||
var trapFocusInsideOverlay = function (event) {
|
||||
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
|
||||
event.stopPropagation();
|
||||
initFocus();
|
||||
var trapFocusInsideOverlay = function (e) {
|
||||
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) {
|
||||
e.stopPropagation();
|
||||
btnClose.focus();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,7 +97,7 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function bindImageClickListeners(selector, userOptions) {
|
||||
var galleryNodeList = document.querySelectorAll(selector);
|
||||
var galleryNodeList = QSA(selector);
|
||||
var selectorData = {
|
||||
galleries: [],
|
||||
nodeList: galleryNodeList
|
||||
@@ -98,28 +106,25 @@ window.baguetteBox = (function () {
|
||||
|
||||
[].forEach.call(galleryNodeList, function (galleryElement) {
|
||||
var tagsNodeList = [];
|
||||
if (galleryElement.tagName === 'A') {
|
||||
if (galleryElement.tagName === 'A')
|
||||
tagsNodeList = [galleryElement];
|
||||
} else {
|
||||
else
|
||||
tagsNodeList = galleryElement.getElementsByTagName('a');
|
||||
}
|
||||
|
||||
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
||||
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
|
||||
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1)
|
||||
return re_i.test(element.href) || re_v.test(element.href);
|
||||
}
|
||||
});
|
||||
if (tagsNodeList.length === 0) {
|
||||
if (!tagsNodeList.length)
|
||||
return;
|
||||
}
|
||||
|
||||
var gallery = [];
|
||||
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
||||
var imageElementClickHandler = function (event) {
|
||||
if (event && (event.ctrlKey || event.metaKey))
|
||||
var imageElementClickHandler = function (e) {
|
||||
if (ctrl(e))
|
||||
return true;
|
||||
|
||||
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
||||
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||
prepareOverlay(gallery, userOptions);
|
||||
showOverlay(imageIndex);
|
||||
};
|
||||
@@ -137,80 +142,94 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function clearCachedData() {
|
||||
for (var selector in data) {
|
||||
if (data.hasOwnProperty(selector)) {
|
||||
for (var selector in data)
|
||||
if (data.hasOwnProperty(selector))
|
||||
removeFromCache(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromCache(selector) {
|
||||
if (!data.hasOwnProperty(selector)) {
|
||||
if (!data.hasOwnProperty(selector))
|
||||
return;
|
||||
}
|
||||
|
||||
var galleries = data[selector].galleries;
|
||||
[].forEach.call(galleries, function (gallery) {
|
||||
[].forEach.call(gallery, function (imageItem) {
|
||||
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
||||
});
|
||||
|
||||
if (currentGallery === gallery) {
|
||||
if (currentGallery === gallery)
|
||||
currentGallery = [];
|
||||
}
|
||||
});
|
||||
|
||||
delete data[selector];
|
||||
}
|
||||
|
||||
function buildOverlay() {
|
||||
overlay = ebi('baguetteBox-overlay');
|
||||
if (overlay) {
|
||||
slider = ebi('baguetteBox-slider');
|
||||
previousButton = ebi('previous-button');
|
||||
nextButton = ebi('next-button');
|
||||
closeButton = ebi('close-button');
|
||||
return;
|
||||
overlay = ebi('bbox-overlay');
|
||||
if (!overlay) {
|
||||
var ctr = mknod('div');
|
||||
ctr.innerHTML = (
|
||||
'<div id="bbox-overlay" role="dialog">' +
|
||||
'<div id="bbox-slider"></div>' +
|
||||
'<button id="bbox-prev" class="bbox-btn" type="button" aria-label="Previous"><</button>' +
|
||||
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">></button>' +
|
||||
'<div id="bbox-btns">' +
|
||||
'<button id="bbox-help" type="button">?</button>' +
|
||||
'<button id="bbox-vmode" type="button" tt="a"></button>' +
|
||||
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
|
||||
'</div></div>'
|
||||
);
|
||||
overlay = ctr.firstChild;
|
||||
QS('body').appendChild(overlay);
|
||||
tt.att(overlay);
|
||||
}
|
||||
overlay = mknod('div');
|
||||
overlay.setAttribute('role', 'dialog');
|
||||
overlay.id = 'baguetteBox-overlay';
|
||||
document.getElementsByTagName('body')[0].appendChild(overlay);
|
||||
|
||||
slider = mknod('div');
|
||||
slider.id = 'baguetteBox-slider';
|
||||
overlay.appendChild(slider);
|
||||
|
||||
previousButton = mknod('button');
|
||||
previousButton.setAttribute('type', 'button');
|
||||
previousButton.id = 'previous-button';
|
||||
previousButton.setAttribute('aria-label', 'Previous');
|
||||
previousButton.innerHTML = '<';
|
||||
overlay.appendChild(previousButton);
|
||||
|
||||
nextButton = mknod('button');
|
||||
nextButton.setAttribute('type', 'button');
|
||||
nextButton.id = 'next-button';
|
||||
nextButton.setAttribute('aria-label', 'Next');
|
||||
nextButton.innerHTML = '>';
|
||||
overlay.appendChild(nextButton);
|
||||
|
||||
closeButton = mknod('button');
|
||||
closeButton.setAttribute('type', 'button');
|
||||
closeButton.id = 'close-button';
|
||||
closeButton.setAttribute('aria-label', 'Close');
|
||||
closeButton.innerHTML = '×';
|
||||
overlay.appendChild(closeButton);
|
||||
|
||||
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
|
||||
|
||||
slider = ebi('bbox-slider');
|
||||
btnPrev = ebi('bbox-prev');
|
||||
btnNext = ebi('bbox-next');
|
||||
btnHelp = ebi('bbox-help');
|
||||
btnVmode = ebi('bbox-vmode');
|
||||
btnClose = ebi('bbox-close');
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
function halp() {
|
||||
if (ebi('bbox-halp'))
|
||||
return;
|
||||
|
||||
var list = [
|
||||
['<b># hotkey</b>', '<b># operation</b>'],
|
||||
['escape', 'close'],
|
||||
['left, J', 'previous file'],
|
||||
['right, L', 'next file'],
|
||||
['home', 'first file'],
|
||||
['end', 'last file'],
|
||||
['space, P, K', 'video: play / pause'],
|
||||
['U', 'video: seek 10sec back'],
|
||||
['P', 'video: seek 10sec ahead'],
|
||||
['M', 'video: toggle mute'],
|
||||
['R', 'video: toggle loop'],
|
||||
['C', 'video: toggle auto-next'],
|
||||
['F', 'video: toggle fullscreen'],
|
||||
],
|
||||
d = mknod('table'),
|
||||
html = ['<tbody>'];
|
||||
|
||||
for (var a = 0; a < list.length; a++)
|
||||
html.push('<tr><td>' + list[a][0] + '</td><td>' + list[a][1] + '</td></tr>');
|
||||
|
||||
d.innerHTML = html.join('\n') + '</tbody>';
|
||||
d.setAttribute('id', 'bbox-halp');
|
||||
d.onclick = function () {
|
||||
overlay.removeChild(d);
|
||||
};
|
||||
overlay.appendChild(d);
|
||||
}
|
||||
|
||||
function keyDownHandler(e) {
|
||||
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||
return;
|
||||
|
||||
var k = e.code + '';
|
||||
var k = e.code + '', v = vid();
|
||||
|
||||
if (k == "ArrowLeft" || k == "KeyJ")
|
||||
showPreviousImage();
|
||||
@@ -226,6 +245,73 @@ window.baguetteBox = (function () {
|
||||
playpause();
|
||||
else if (k == "KeyU" || k == "KeyO")
|
||||
relseek(k == "KeyU" ? -10 : 10);
|
||||
else if (k == "KeyM" && v) {
|
||||
v.muted = vmute = !vmute;
|
||||
mp_ctl();
|
||||
}
|
||||
else if (k == "KeyR" && v) {
|
||||
vloop = !vloop;
|
||||
vnext = vnext && !vloop;
|
||||
setVmode();
|
||||
}
|
||||
else if (k == "KeyC" && v) {
|
||||
vnext = !vnext;
|
||||
vloop = vloop && !vnext;
|
||||
setVmode();
|
||||
}
|
||||
else if (k == "KeyF")
|
||||
try {
|
||||
if (isFullscreen)
|
||||
document.exitFullscreen();
|
||||
else
|
||||
v.requestFullscreen();
|
||||
}
|
||||
catch (ex) { }
|
||||
}
|
||||
|
||||
function setVmode() {
|
||||
var v = vid();
|
||||
ebi('bbox-vmode').style.display = v ? '' : 'none';
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
var msg = 'When video ends, ', tts = '', lbl;
|
||||
if (vloop) {
|
||||
lbl = 'Loop';
|
||||
msg += 'repeat it';
|
||||
tts = '$NHotkey: R';
|
||||
}
|
||||
else if (vnext) {
|
||||
lbl = 'Cont';
|
||||
msg += 'continue to next';
|
||||
tts = '$NHotkey: C';
|
||||
}
|
||||
else {
|
||||
lbl = 'Stop';
|
||||
msg += 'just stop'
|
||||
}
|
||||
btnVmode.setAttribute('aria-label', msg);
|
||||
btnVmode.setAttribute('tt', msg + tts);
|
||||
btnVmode.textContent = lbl;
|
||||
|
||||
v.loop = vloop
|
||||
if (vloop && v.paused)
|
||||
v.play();
|
||||
}
|
||||
|
||||
function tglVmode() {
|
||||
if (vloop) {
|
||||
vnext = true;
|
||||
vloop = false;
|
||||
}
|
||||
else if (vnext)
|
||||
vnext = false;
|
||||
else
|
||||
vloop = true;
|
||||
|
||||
setVmode();
|
||||
if (tt.en)
|
||||
tt.show.bind(this)();
|
||||
}
|
||||
|
||||
function keyUpHandler(e) {
|
||||
@@ -257,9 +343,11 @@ window.baguetteBox = (function () {
|
||||
|
||||
function bindEvents() {
|
||||
bind(overlay, 'click', overlayClickHandler);
|
||||
bind(previousButton, 'click', showPreviousImage);
|
||||
bind(nextButton, 'click', showNextImage);
|
||||
bind(closeButton, 'click', hideOverlay);
|
||||
bind(btnPrev, 'click', showPreviousImage);
|
||||
bind(btnNext, 'click', showNextImage);
|
||||
bind(btnClose, 'click', hideOverlay);
|
||||
bind(btnVmode, 'click', tglVmode);
|
||||
bind(btnHelp, 'click', halp);
|
||||
bind(slider, 'contextmenu', contextmenuHandler);
|
||||
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||
@@ -269,9 +357,11 @@ window.baguetteBox = (function () {
|
||||
|
||||
function unbindEvents() {
|
||||
unbind(overlay, 'click', overlayClickHandler);
|
||||
unbind(previousButton, 'click', showPreviousImage);
|
||||
unbind(nextButton, 'click', showNextImage);
|
||||
unbind(closeButton, 'click', hideOverlay);
|
||||
unbind(btnPrev, 'click', showPreviousImage);
|
||||
unbind(btnNext, 'click', showNextImage);
|
||||
unbind(btnClose, 'click', hideOverlay);
|
||||
unbind(btnVmode, 'click', tglVmode);
|
||||
unbind(btnHelp, 'click', halp);
|
||||
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||
@@ -280,9 +370,9 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function prepareOverlay(gallery, userOptions) {
|
||||
if (currentGallery === gallery) {
|
||||
if (currentGallery === gallery)
|
||||
return;
|
||||
}
|
||||
|
||||
currentGallery = gallery;
|
||||
setOptions(userOptions);
|
||||
slider.innerHTML = '';
|
||||
@@ -296,8 +386,8 @@ window.baguetteBox = (function () {
|
||||
fullImage.id = 'baguette-img-' + i;
|
||||
imagesElements.push(fullImage);
|
||||
|
||||
imagesFiguresIds.push('baguetteBox-figure-' + i);
|
||||
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
|
||||
imagesFiguresIds.push('bbox-figure-' + i);
|
||||
imagesCaptionsIds.push('bbox-figcaption-' + i);
|
||||
slider.appendChild(imagesElements[i]);
|
||||
}
|
||||
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
||||
@@ -305,23 +395,21 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function setOptions(newOptions) {
|
||||
if (!newOptions) {
|
||||
if (!newOptions)
|
||||
newOptions = {};
|
||||
}
|
||||
|
||||
for (var item in defaults) {
|
||||
options[item] = defaults[item];
|
||||
if (typeof newOptions[item] !== 'undefined') {
|
||||
if (typeof newOptions[item] !== 'undefined')
|
||||
options[item] = newOptions[item];
|
||||
}
|
||||
}
|
||||
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
||||
options.animation === 'slideIn' ? '' : 'none');
|
||||
|
||||
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
|
||||
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1))
|
||||
options.buttons = false;
|
||||
}
|
||||
|
||||
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
|
||||
btnPrev.style.display = btnNext.style.display = (options.buttons ? '' : 'none');
|
||||
}
|
||||
|
||||
function showOverlay(chosenImageIndex) {
|
||||
@@ -329,12 +417,12 @@ window.baguetteBox = (function () {
|
||||
document.documentElement.style.overflowY = 'hidden';
|
||||
document.body.style.overflowY = 'scroll';
|
||||
}
|
||||
if (overlay.style.display === 'block') {
|
||||
if (overlay.style.display === 'block')
|
||||
return;
|
||||
}
|
||||
|
||||
bind(document, 'keydown', keyDownHandler);
|
||||
bind(document, 'keyup', keyUpHandler);
|
||||
bind(document, 'fullscreenchange', onFSC);
|
||||
currentIndex = chosenImageIndex;
|
||||
touch = {
|
||||
count: 0,
|
||||
@@ -351,27 +439,19 @@ window.baguetteBox = (function () {
|
||||
// Fade in overlay
|
||||
setTimeout(function () {
|
||||
overlay.className = 'visible';
|
||||
if (options.bodyClass && document.body.classList) {
|
||||
if (options.bodyClass && document.body.classList)
|
||||
document.body.classList.add(options.bodyClass);
|
||||
}
|
||||
if (options.afterShow) {
|
||||
options.afterShow();
|
||||
}
|
||||
}, 50);
|
||||
if (options.onChange) {
|
||||
options.onChange(currentIndex, imagesElements.length);
|
||||
}
|
||||
documentLastFocus = document.activeElement;
|
||||
initFocus();
|
||||
isOverlayVisible = true;
|
||||
}
|
||||
|
||||
function initFocus() {
|
||||
if (options.buttons) {
|
||||
previousButton.focus();
|
||||
} else {
|
||||
closeButton.focus();
|
||||
}
|
||||
if (options.afterShow)
|
||||
options.afterShow();
|
||||
}, 50);
|
||||
|
||||
if (options.onChange)
|
||||
options.onChange(currentIndex, imagesElements.length);
|
||||
|
||||
documentLastFocus = document.activeElement;
|
||||
btnClose.focus();
|
||||
isOverlayVisible = true;
|
||||
}
|
||||
|
||||
function hideOverlay(e) {
|
||||
@@ -381,22 +461,26 @@ window.baguetteBox = (function () {
|
||||
document.documentElement.style.overflowY = 'auto';
|
||||
document.body.style.overflowY = 'auto';
|
||||
}
|
||||
if (overlay.style.display === 'none') {
|
||||
if (overlay.style.display === 'none')
|
||||
return;
|
||||
}
|
||||
|
||||
unbind(document, 'keydown', keyDownHandler);
|
||||
unbind(document, 'keyup', keyUpHandler);
|
||||
unbind(document, 'fullscreenchange', onFSC);
|
||||
// Fade out and hide the overlay
|
||||
overlay.className = '';
|
||||
setTimeout(function () {
|
||||
overlay.style.display = 'none';
|
||||
if (options.bodyClass && document.body.classList) {
|
||||
if (options.bodyClass && document.body.classList)
|
||||
document.body.classList.remove(options.bodyClass);
|
||||
}
|
||||
if (options.afterHide) {
|
||||
|
||||
var h = ebi('bbox-halp');
|
||||
if (h)
|
||||
h.parentNode.removeChild(h);
|
||||
|
||||
if (options.afterHide)
|
||||
options.afterHide();
|
||||
}
|
||||
|
||||
documentLastFocus && documentLastFocus.focus();
|
||||
isOverlayVisible = false;
|
||||
}, 500);
|
||||
@@ -406,63 +490,68 @@ window.baguetteBox = (function () {
|
||||
var imageContainer = imagesElements[index];
|
||||
var galleryItem = currentGallery[index];
|
||||
|
||||
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
|
||||
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined')
|
||||
return; // out-of-bounds or gallery dirty
|
||||
}
|
||||
|
||||
if (imageContainer.querySelector('img, video')) {
|
||||
if (imageContainer.querySelector('img, video'))
|
||||
// was loaded, cb and bail
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
return callback ? callback() : null;
|
||||
|
||||
// maybe unloaded video
|
||||
while (imageContainer.firstChild)
|
||||
imageContainer.removeChild(imageContainer.firstChild);
|
||||
|
||||
var imageElement = galleryItem.imageElement,
|
||||
imageSrc = imageElement.href,
|
||||
is_vid = re_v.test(imageSrc),
|
||||
thumbnailElement = imageElement.querySelector('img, video'),
|
||||
imageCaption = typeof options.captions === 'function' ?
|
||||
options.captions.call(currentGallery, imageElement) :
|
||||
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||
|
||||
imageSrc += imageSrc.indexOf('?') < 0 ? '?cache' : '&cache';
|
||||
|
||||
if (is_vid && index != currentIndex)
|
||||
return; // no preload
|
||||
|
||||
var figure = mknod('figure');
|
||||
figure.id = 'baguetteBox-figure-' + index;
|
||||
figure.innerHTML = '<div class="baguetteBox-spinner">' +
|
||||
'<div class="baguetteBox-double-bounce1"></div>' +
|
||||
'<div class="baguetteBox-double-bounce2"></div>' +
|
||||
figure.id = 'bbox-figure-' + index;
|
||||
figure.innerHTML = '<div class="bbox-spinner">' +
|
||||
'<div class="bbox-double-bounce1"></div>' +
|
||||
'<div class="bbox-double-bounce2"></div>' +
|
||||
'</div>';
|
||||
|
||||
if (options.captions && imageCaption) {
|
||||
var figcaption = mknod('figcaption');
|
||||
figcaption.id = 'baguetteBox-figcaption-' + index;
|
||||
figcaption.id = 'bbox-figcaption-' + index;
|
||||
figcaption.innerHTML = imageCaption;
|
||||
figure.appendChild(figcaption);
|
||||
}
|
||||
imageContainer.appendChild(figure);
|
||||
|
||||
var is_vid = re_v.test(imageSrc),
|
||||
image = mknod(is_vid ? 'video' : 'img');
|
||||
|
||||
var image = mknod(is_vid ? 'video' : 'img');
|
||||
clmod(imageContainer, 'vid', is_vid);
|
||||
|
||||
image.addEventListener(is_vid ? 'loadedmetadata' : 'load', function () {
|
||||
// Remove loader element
|
||||
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
|
||||
var spinner = QS('#baguette-img-' + index + ' .bbox-spinner');
|
||||
figure.removeChild(spinner);
|
||||
if (!options.async && callback)
|
||||
callback();
|
||||
});
|
||||
image.setAttribute('src', imageSrc);
|
||||
image.setAttribute('controls', 'controls');
|
||||
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||
if (options.titleTag && imageCaption) {
|
||||
image.title = imageCaption;
|
||||
if (is_vid) {
|
||||
image.setAttribute('controls', 'controls');
|
||||
image.onended = vidEnd;
|
||||
}
|
||||
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||
if (options.titleTag && imageCaption)
|
||||
image.title = imageCaption;
|
||||
|
||||
figure.appendChild(image);
|
||||
|
||||
if (options.async && callback) {
|
||||
if (options.async && callback)
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function showNextImage(e) {
|
||||
@@ -475,26 +564,20 @@ window.baguetteBox = (function () {
|
||||
return show(currentIndex - 1);
|
||||
}
|
||||
|
||||
function showFirstImage(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
function showFirstImage(e) {
|
||||
if (e)
|
||||
e.preventDefault();
|
||||
|
||||
return show(0);
|
||||
}
|
||||
|
||||
function showLastImage(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
function showLastImage(e) {
|
||||
if (e)
|
||||
e.preventDefault();
|
||||
|
||||
return show(currentGallery.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the gallery to a specific index
|
||||
* @param `index` {number} - the position of the image
|
||||
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
|
||||
* @return {boolean} - true on success or false if the index is invalid
|
||||
*/
|
||||
function show(index, gallery) {
|
||||
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
||||
prepareOverlay(gallery, options);
|
||||
@@ -502,19 +585,25 @@ window.baguetteBox = (function () {
|
||||
return true;
|
||||
}
|
||||
if (index < 0) {
|
||||
if (options.animation) {
|
||||
if (options.animation)
|
||||
bounceAnimation('left');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
if (index >= imagesElements.length) {
|
||||
if (options.animation) {
|
||||
if (options.animation)
|
||||
bounceAnimation('right');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
playvid(false);
|
||||
var v = vid();
|
||||
if (v) {
|
||||
v.src = '';
|
||||
v.load();
|
||||
v.parentNode.removeChild(v);
|
||||
}
|
||||
|
||||
currentIndex = index;
|
||||
loadImage(currentIndex, function () {
|
||||
preloadNext(currentIndex);
|
||||
@@ -522,9 +611,8 @@ window.baguetteBox = (function () {
|
||||
});
|
||||
updateOffset();
|
||||
|
||||
if (options.onChange) {
|
||||
if (options.onChange)
|
||||
options.onChange(currentIndex, imagesElements.length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -549,10 +637,23 @@ window.baguetteBox = (function () {
|
||||
vid().currentTime += sec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the bounce animation
|
||||
* @param {('left'|'right')} direction - Direction of the movement
|
||||
*/
|
||||
function vidEnd() {
|
||||
if (this == vid() && vnext)
|
||||
showNextImage();
|
||||
}
|
||||
|
||||
function mp_ctl() {
|
||||
var v = vid();
|
||||
if (!vmute && v && mp.au && !mp.au.paused) {
|
||||
mp.fade_out();
|
||||
resume_mp = true;
|
||||
}
|
||||
else if (resume_mp && (vmute || !v) && mp.au && mp.au.paused) {
|
||||
mp.fade_in();
|
||||
resume_mp = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bounceAnimation(direction) {
|
||||
slider.className = 'bounce-from-' + direction;
|
||||
setTimeout(function () {
|
||||
@@ -572,22 +673,29 @@ window.baguetteBox = (function () {
|
||||
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
||||
}
|
||||
playvid(false);
|
||||
playvid(true);
|
||||
var v = vid();
|
||||
if (v) {
|
||||
playvid(true);
|
||||
v.muted = vmute;
|
||||
v.loop = vloop;
|
||||
}
|
||||
mp_ctl();
|
||||
setVmode();
|
||||
}
|
||||
|
||||
function preloadNext(index) {
|
||||
if (index - currentIndex >= options.preload) {
|
||||
if (index - currentIndex >= options.preload)
|
||||
return;
|
||||
}
|
||||
|
||||
loadImage(index + 1, function () {
|
||||
preloadNext(index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
function preloadPrev(index) {
|
||||
if (currentIndex - index >= options.preload) {
|
||||
if (currentIndex - index >= options.preload)
|
||||
return;
|
||||
}
|
||||
|
||||
loadImage(index - 1, function () {
|
||||
preloadPrev(index - 1);
|
||||
});
|
||||
@@ -606,7 +714,7 @@ window.baguetteBox = (function () {
|
||||
clearCachedData();
|
||||
unbind(document, 'keydown', keyDownHandler);
|
||||
unbind(document, 'keyup', keyUpHandler);
|
||||
document.getElementsByTagName('body')[0].removeChild(ebi('baguetteBox-overlay'));
|
||||
document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));
|
||||
data = {};
|
||||
currentGallery = [];
|
||||
currentIndex = 0;
|
||||
|
||||
@@ -211,15 +211,39 @@ a, #files tbody div a:last-child {
|
||||
margin: .8em 0;
|
||||
}
|
||||
#srv_info {
|
||||
opacity: .5;
|
||||
font-size: .8em;
|
||||
color: #fc5;
|
||||
color: #a73;
|
||||
background: #333;
|
||||
position: absolute;
|
||||
top: .5em;
|
||||
font-size: .8em;
|
||||
top: .5em;
|
||||
left: 2em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
#srv_info span {
|
||||
color: #fff;
|
||||
color: #aaa;
|
||||
}
|
||||
#acc_info {
|
||||
position: absolute;
|
||||
font-size: .81em;
|
||||
top: .5em;
|
||||
right: 2em;
|
||||
color: #999;
|
||||
}
|
||||
#acc_info span:before {
|
||||
color: #f4c;
|
||||
border-bottom: 1px solid rgba(255,68,204,0.6);
|
||||
margin-right: .6em;
|
||||
}
|
||||
html.read #acc_info span:before {
|
||||
content: 'Read-Only access';
|
||||
}
|
||||
html.write #acc_info span:before {
|
||||
content: 'Write-Only access';
|
||||
}
|
||||
html.read.write #acc_info span:before {
|
||||
content: 'Read-Write access';
|
||||
color: #999;
|
||||
border: none;
|
||||
}
|
||||
#files tbody a.play {
|
||||
color: #e70;
|
||||
@@ -337,10 +361,18 @@ html.light #ggrid a.sel {
|
||||
height: 100%;
|
||||
background: #3c3c3c;
|
||||
}
|
||||
#wtgrid,
|
||||
#wtico {
|
||||
cursor: url(/.cpr/dd/4.png), pointer;
|
||||
animation: cursor 500ms;
|
||||
position: relative;
|
||||
top: -.06em;
|
||||
}
|
||||
#wtgrid {
|
||||
font-size: .8em;
|
||||
top: -.12em;
|
||||
}
|
||||
#wtgrid:hover,
|
||||
#wtico:hover {
|
||||
animation: cursor 500ms infinite;
|
||||
}
|
||||
@@ -356,9 +388,10 @@ html.light #ggrid a.sel {
|
||||
}
|
||||
#wtoggle {
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
top: -1.2em;
|
||||
right: 0;
|
||||
width: 1.2em;
|
||||
width: 2.5em;
|
||||
height: 1em;
|
||||
font-size: 2em;
|
||||
line-height: 1em;
|
||||
@@ -390,10 +423,10 @@ html.light #ggrid a.sel {
|
||||
line-height: 1em;
|
||||
}
|
||||
#wtoggle.np {
|
||||
width: 5.5em;
|
||||
width: 6.63em;
|
||||
}
|
||||
#wtoggle.sel {
|
||||
width: 6.4em;
|
||||
width: 7.57em;
|
||||
}
|
||||
#wtoggle.sel #wzip,
|
||||
#wtoggle.np #wnp {
|
||||
@@ -476,6 +509,17 @@ html.light #ggrid a.sel {
|
||||
max-width: 9em;
|
||||
}
|
||||
}
|
||||
@media (max-width: 35em) {
|
||||
#ops>a[data-dest="new_md"],
|
||||
#ops>a[data-dest="msg"] {
|
||||
display: none;
|
||||
}
|
||||
#op_mkdir.act+div,
|
||||
#op_mkdir.act+div+div {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -773,9 +817,14 @@ input.eq_gain {
|
||||
display: block;
|
||||
width: 1em;
|
||||
border-radius: .2em;
|
||||
margin: -1.3em auto 0 auto;
|
||||
margin: -1.2em auto 0 auto;
|
||||
top: 2em;
|
||||
position: relative;
|
||||
background: #444;
|
||||
}
|
||||
#files th span {
|
||||
position: relative;
|
||||
}
|
||||
#files>thead>tr>th.min,
|
||||
#files td.min {
|
||||
display: none;
|
||||
@@ -902,7 +951,8 @@ html.light #ggrid a:hover {
|
||||
#pvol,
|
||||
#barbuf,
|
||||
#barpos,
|
||||
#u2conf label {
|
||||
#u2conf label,
|
||||
#ops {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
@@ -975,10 +1025,14 @@ html.light .tgl.btn.on {
|
||||
}
|
||||
html.light #srv_info {
|
||||
color: #c83;
|
||||
background: #eee;
|
||||
}
|
||||
html.light #srv_info,
|
||||
html.light #acc_info {
|
||||
text-shadow: 1px 1px 0 #fff;
|
||||
}
|
||||
html.light #srv_info span {
|
||||
color: #000;
|
||||
color: #777;
|
||||
}
|
||||
html.light #treeul a+a {
|
||||
background: inherit;
|
||||
@@ -1145,7 +1199,7 @@ html.light #tree::-webkit-scrollbar {
|
||||
|
||||
|
||||
|
||||
#baguetteBox-overlay {
|
||||
#bbox-overlay {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
@@ -1155,62 +1209,66 @@ html.light #tree::-webkit-scrollbar {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000000;
|
||||
z-index: 10;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
#baguetteBox-overlay.visible {
|
||||
#bbox-overlay.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
#baguetteBox-overlay .full-image {
|
||||
.full-image {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
#baguetteBox-overlay .full-image figure {
|
||||
.full-image figure {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#baguetteBox-overlay .full-image img,
|
||||
#baguetteBox-overlay .full-image video {
|
||||
.full-image img,
|
||||
.full-image video {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
max-height: calc(100% - 1.4em);
|
||||
margin-bottom: 1.4em;
|
||||
vertical-align: middle;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
#baguetteBox-overlay .full-image video {
|
||||
.full-image video {
|
||||
background: #333;
|
||||
}
|
||||
#baguetteBox-overlay .full-image figcaption {
|
||||
.full-image figcaption {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
bottom: .1em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 1.8;
|
||||
white-space: normal;
|
||||
color: #ccc;
|
||||
}
|
||||
#baguetteBox-overlay figcaption a {
|
||||
#bbox-overlay figcaption a {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: .4em;
|
||||
padding: .3em .6em;
|
||||
}
|
||||
#baguetteBox-overlay .full-image:before {
|
||||
html.light #bbox-overlay figcaption a {
|
||||
color: #0bf;
|
||||
}
|
||||
.full-image:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
width: 1px;
|
||||
margin-right: -1px;
|
||||
}
|
||||
#baguetteBox-slider {
|
||||
position: absolute;
|
||||
#bbox-slider {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
@@ -1218,10 +1276,10 @@ html.light #tree::-webkit-scrollbar {
|
||||
white-space: nowrap;
|
||||
transition: left .2s ease, transform .2s ease;
|
||||
}
|
||||
#baguetteBox-slider.bounce-from-right {
|
||||
.bounce-from-right {
|
||||
animation: bounceFromRight .4s ease-out;
|
||||
}
|
||||
#baguetteBox-slider.bounce-from-left {
|
||||
.bounce-from-left {
|
||||
animation: bounceFromLeft .4s ease-out;
|
||||
}
|
||||
@keyframes bounceFromRight {
|
||||
@@ -1234,48 +1292,63 @@ html.light #tree::-webkit-scrollbar {
|
||||
50% {margin-left: 30px}
|
||||
100% {margin-left: 0}
|
||||
}
|
||||
.baguetteBox-button#next-button,
|
||||
.baguetteBox-button#previous-button {
|
||||
#bbox-next,
|
||||
#bbox-prev {
|
||||
top: 50%;
|
||||
top: calc(50% - 30px);
|
||||
width: 44px;
|
||||
height: 60px;
|
||||
}
|
||||
.baguetteBox-button {
|
||||
position: absolute;
|
||||
.bbox-btn {
|
||||
position: fixed;
|
||||
}
|
||||
#bbox-overlay button {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0 .3em;
|
||||
margin: 0 .4em;
|
||||
border: 0;
|
||||
border-radius: 15%;
|
||||
background: rgba(50, 50, 50, 0.5);
|
||||
color: #ddd;
|
||||
font: 1.6em sans-serif;
|
||||
color: rgba(255,255,255,0.7);
|
||||
transition: background-color .3s ease;
|
||||
transition: color .3s ease;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.4em;
|
||||
vertical-align: top;
|
||||
}
|
||||
.baguetteBox-button:focus,
|
||||
.baguetteBox-button:hover {
|
||||
#bbox-overlay button:focus,
|
||||
#bbox-overlay button:hover {
|
||||
color: rgba(255,255,255,0.9);
|
||||
background: rgba(50, 50, 50, 0.9);
|
||||
}
|
||||
#next-button {
|
||||
#bbox-next {
|
||||
right: 1%;
|
||||
}
|
||||
#bbox-prev {
|
||||
left: 1%;
|
||||
}
|
||||
#bbox-btns {
|
||||
top: .5em;
|
||||
right: 2%;
|
||||
position: fixed;
|
||||
}
|
||||
#previous-button {
|
||||
left: 2%;
|
||||
}
|
||||
#close-button {
|
||||
top: 20px;
|
||||
right: 2%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.baguetteBox-button svg {
|
||||
#bbox-halp {
|
||||
color: #fff;
|
||||
background: #333;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
padding: .4em;
|
||||
}
|
||||
.baguetteBox-spinner {
|
||||
#bbox-halp td {
|
||||
padding: .2em .5em;
|
||||
}
|
||||
#bbox-halp td:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
.bbox-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: inline-block;
|
||||
@@ -1285,8 +1358,8 @@ html.light #tree::-webkit-scrollbar {
|
||||
margin-top: -20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.baguetteBox-double-bounce1,
|
||||
.baguetteBox-double-bounce2 {
|
||||
.bbox-double-bounce1,
|
||||
.bbox-double-bounce2 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
@@ -1297,7 +1370,7 @@ html.light #tree::-webkit-scrollbar {
|
||||
left: 0;
|
||||
animation: bounce 2s infinite ease-in-out;
|
||||
}
|
||||
.baguetteBox-double-bounce2 {
|
||||
.bbox-double-bounce2 {
|
||||
animation-delay: -1s;
|
||||
}
|
||||
@keyframes bounce {
|
||||
|
||||
@@ -39,23 +39,23 @@
|
||||
<div id="op_mkdir" class="opview opbox act">
|
||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||
<input type="hidden" name="act" value="mkdir" />
|
||||
<input type="text" name="name" size="30">
|
||||
<input type="submit" value="mkdir">
|
||||
📂<input type="text" name="name" size="30">
|
||||
<input type="submit" value="make directory">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="op_new_md" class="opview opbox">
|
||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||
<input type="hidden" name="act" value="new_md" />
|
||||
<input type="text" name="name" size="30">
|
||||
<input type="submit" value="create doc">
|
||||
📝<input type="text" name="name" size="30">
|
||||
<input type="submit" value="new markdown doc">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="op_msg" class="opview opbox act">
|
||||
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
||||
<input type="text" name="msg" size="30">
|
||||
<input type="submit" value="send msg">
|
||||
📟<input type="text" name="msg" size="30">
|
||||
<input type="submit" value="send msg to server log">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
<div id="widget"></div>
|
||||
|
||||
<script>
|
||||
var perms = {{ perms }},
|
||||
var acct = "{{ acct }}",
|
||||
perms = {{ perms }},
|
||||
tag_order_cfg = {{ tag_order }},
|
||||
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||
|
||||
@@ -37,6 +37,7 @@ ebi('widget').innerHTML = (
|
||||
' href="#" id="npirc" tt="copy irc-formatted track info">📋irc</a><a' +
|
||||
' href="#" id="nptxt" tt="copy plaintext track info">📋txt</a>' +
|
||||
'</span><a' +
|
||||
' href="#" id="wtgrid">田</a><a' +
|
||||
' href="#" id="wtico">♫</a>' +
|
||||
'</div>' +
|
||||
'<div id="widgeti">' +
|
||||
@@ -149,8 +150,8 @@ ebi('op_cfg').innerHTML = (
|
||||
ebi('tree').innerHTML = (
|
||||
'<div id="treeh">\n' +
|
||||
' <a href="#" id="detree" tt="show breadcrumbs$NHotkey: B">🍞...</a>\n' +
|
||||
' <a href="#" class="btn" step="2" id="twobytwo">+</a>\n' +
|
||||
' <a href="#" class="btn" step="-2" id="twig">–</a>\n' +
|
||||
' <a href="#" class="btn" step="2" id="twobytwo" tt="Hotkey: A">+</a>\n' +
|
||||
' <a href="#" class="btn" step="-2" id="twig" tt="Hotkey: D">–</a>\n' +
|
||||
' <a href="#" class="tgl btn" id="dyntree" tt="autogrow as tree expands">a</a>\n' +
|
||||
'</div>\n' +
|
||||
'<ul id="treeul"></ul>\n' +
|
||||
@@ -167,16 +168,19 @@ ebi('tree').innerHTML = (
|
||||
|
||||
|
||||
function opclick(e) {
|
||||
ev(e);
|
||||
|
||||
var dest = this.getAttribute('data-dest');
|
||||
swrite('opmode', dest || null);
|
||||
if (ctrl(e))
|
||||
return;
|
||||
|
||||
ev(e);
|
||||
goto(dest);
|
||||
|
||||
swrite('opmode', dest || null);
|
||||
|
||||
var input = QS('.opview.act input:not([type="hidden"])')
|
||||
if (input && !is_touch)
|
||||
if (input && !is_touch) {
|
||||
tt.skip = true;
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -604,7 +608,7 @@ var widget = (function () {
|
||||
|
||||
m += '[' + cv + s2ms(mp.au.currentTime) + ck + '/' + cv + s2ms(mp.au.duration) + ck + ']';
|
||||
|
||||
var o = document.createElement('input');
|
||||
var o = mknod('input');
|
||||
o.style.cssText = 'position:fixed;top:45%;left:48%;padding:1em;z-index:9';
|
||||
o.value = m;
|
||||
document.body.appendChild(o);
|
||||
@@ -1142,7 +1146,7 @@ var audio_eq = (function () {
|
||||
v = parseFloat(vs);
|
||||
|
||||
if (isNaN(v) || v + '' != vs)
|
||||
throw 42;
|
||||
throw new Error('inval band');
|
||||
|
||||
if (isNaN(band))
|
||||
r.amp = Math.round((v + step * 0.2) * 100) / 100;
|
||||
@@ -1448,17 +1452,24 @@ function play_linked() {
|
||||
};
|
||||
|
||||
|
||||
(function () {
|
||||
var d = mknod('div');
|
||||
d.setAttribute('id', 'acc_info');
|
||||
document.body.insertBefore(d, ebi('ops'));
|
||||
})();
|
||||
|
||||
|
||||
var thegrid = (function () {
|
||||
var lfiles = ebi('files'),
|
||||
gfiles = document.createElement('div');
|
||||
gfiles = mknod('div');
|
||||
|
||||
gfiles.setAttribute('id', 'gfiles');
|
||||
gfiles.style.display = 'none';
|
||||
gfiles.innerHTML = (
|
||||
'<div id="ghead">' +
|
||||
'<a href="#" class="tgl btn" id="gridsel" tt="enable file selection; ctrl-click a file to override$NHotkey: S">multiselect</a> zoom ' +
|
||||
'<a href="#" class="btn" z="-1.2" tt="Hotkey: A">–</a> ' +
|
||||
'<a href="#" class="btn" z="1.2" tt="Hotkey: D">+</a> sort by: ' +
|
||||
'<a href="#" class="btn" z="-1.2" tt="Hotkey: shift-A">–</a> ' +
|
||||
'<a href="#" class="btn" z="1.2" tt="Hotkey: shift-D">+</a> sort by: ' +
|
||||
'<a href="#" s="href">name</a>, ' +
|
||||
'<a href="#" s="sz">size</a>, ' +
|
||||
'<a href="#" s="ts">date</a>, ' +
|
||||
@@ -1484,8 +1495,11 @@ var thegrid = (function () {
|
||||
r.setdirty();
|
||||
};
|
||||
|
||||
ebi('griden').onclick = function (e) {
|
||||
ebi('griden').onclick = ebi('wtgrid').onclick = function (e) {
|
||||
ev(e);
|
||||
if (!this.closest)
|
||||
return;
|
||||
|
||||
r.en = !r.en;
|
||||
bcfg_set('griden', r.en);
|
||||
if (r.en) {
|
||||
@@ -1552,7 +1566,7 @@ var thegrid = (function () {
|
||||
setsz();
|
||||
|
||||
function gclick(e) {
|
||||
if (e && (e.ctrlKey || e.metaKey))
|
||||
if (ctrl(e))
|
||||
return true;
|
||||
|
||||
var oth = ebi(this.getAttribute('ref')),
|
||||
@@ -1706,6 +1720,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) {
|
||||
var links = QSA('#treeul li>a+a');
|
||||
if (!links.length) {
|
||||
@@ -1731,6 +1765,7 @@ function tree_neigh(n) {
|
||||
if (act >= links.length)
|
||||
act = 0;
|
||||
|
||||
treectl.dir_cb = tree_scrollto;
|
||||
links[act].click();
|
||||
}
|
||||
|
||||
@@ -1756,6 +1791,9 @@ document.onkeydown = function (e) {
|
||||
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||
return;
|
||||
|
||||
if (QS('#bbox-overlay.visible'))
|
||||
return;
|
||||
|
||||
var k = e.code + '', pos = -1, n;
|
||||
|
||||
if (e.shiftKey && k != 'KeyA' && k != 'KeyD')
|
||||
@@ -1932,7 +1970,7 @@ document.onkeydown = function (e) {
|
||||
for (var b = 1; b < sconf[a].length; b++) {
|
||||
var k = sconf[a][b][0],
|
||||
chk = 'srch_' + k + 'c',
|
||||
tvs = ebi('srch_' + k + 'v').value.split(/ /g);
|
||||
tvs = ebi('srch_' + k + 'v').value.split(/ +/g);
|
||||
|
||||
if (!ebi(chk).checked)
|
||||
continue;
|
||||
@@ -1945,7 +1983,7 @@ document.onkeydown = function (e) {
|
||||
q += ' and ';
|
||||
|
||||
if (k == 'adv') {
|
||||
q += tv.replace(/ /g, " and ").replace(/([=!><]=?)/, " $1 ");
|
||||
q += tv.replace(/ +/g, " and ").replace(/([=!><]=?)/, " $1 ");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2276,7 +2314,12 @@ var treectl = (function () {
|
||||
var fun = treectl.dir_cb;
|
||||
if (fun) {
|
||||
treectl.dir_cb = null;
|
||||
fun();
|
||||
try {
|
||||
fun();
|
||||
}
|
||||
catch (ex) {
|
||||
console.log("dir_cb failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2297,7 +2340,7 @@ var treectl = (function () {
|
||||
}
|
||||
|
||||
function treego(e) {
|
||||
if (e && (e.ctrlKey || e.metaKey))
|
||||
if (ctrl(e))
|
||||
return true;
|
||||
|
||||
ev(e);
|
||||
@@ -2398,6 +2441,7 @@ var treectl = (function () {
|
||||
if (this.hpush)
|
||||
hist_push(this.top);
|
||||
|
||||
acct = res.acct;
|
||||
apply_perms(res.perms);
|
||||
despin('#files');
|
||||
despin('#gfiles');
|
||||
@@ -2514,9 +2558,13 @@ function despin(sel) {
|
||||
function apply_perms(newperms) {
|
||||
perms = newperms || [];
|
||||
|
||||
ebi('acc_info').innerHTML = '<span>' + (acct != '*' ?
|
||||
'<a href="?pw=x">Logout ' + acct + '</a>' :
|
||||
'<a href="?h">Login</a>') + '</span>';
|
||||
|
||||
var o = QSA('#ops>a[data-perm], #u2footfoot');
|
||||
for (var a = 0; a < o.length; a++) {
|
||||
var display = 'inline';
|
||||
var display = '';
|
||||
var needed = o[a].getAttribute('data-perm').split(' ');
|
||||
for (var b = 0; b < needed.length; b++) {
|
||||
if (!has(perms, needed[b])) {
|
||||
@@ -2537,12 +2585,10 @@ function apply_perms(newperms) {
|
||||
de = document.documentElement,
|
||||
tds = QSA('#u2conf td');
|
||||
|
||||
/* good idea maybe
|
||||
clmod(de, "read", have_read);
|
||||
clmod(de, "write", have_write);
|
||||
clmod(de, "nread", !have_read);
|
||||
clmod(de, "nwrite", !have_write);
|
||||
*/
|
||||
|
||||
for (var a = 0; a < tds.length; a++) {
|
||||
tds[a].style.display =
|
||||
@@ -2568,7 +2614,7 @@ function find_file_col(txt) {
|
||||
for (var a = 0; a < tds.length; a++) {
|
||||
var spans = tds[a].getElementsByTagName('span');
|
||||
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;
|
||||
break;
|
||||
}
|
||||
@@ -2610,14 +2656,36 @@ function mk_files_header(taglist) {
|
||||
|
||||
|
||||
var filecols = (function () {
|
||||
var hidden = jread('filecols', []);
|
||||
var hidden = jread('filecols', []),
|
||||
tts = {
|
||||
"c": "action buttons",
|
||||
"dur": "duration",
|
||||
"q": "quality / bitrate",
|
||||
"Ac": "audio codec",
|
||||
"Vc": "video codec",
|
||||
"Res": "resolution",
|
||||
"T": "filetype",
|
||||
"aq": "audio quality / bitrate",
|
||||
"vq": "video quality / bitrate",
|
||||
"pixfmt": "subsampling / pixel structure",
|
||||
"resw": "horizontal resolution",
|
||||
"resh": "veritcal resolution",
|
||||
"acs": "audio channels",
|
||||
"hz": "sample rate"
|
||||
};
|
||||
|
||||
var add_btns = function () {
|
||||
var ths = QSA('#files th>span');
|
||||
for (var a = 0, aa = ths.length; a < aa; a++) {
|
||||
var th = ths[a].parentElement;
|
||||
var th = ths[a].parentElement,
|
||||
ttv = tts[ths[a].textContent];
|
||||
|
||||
th.innerHTML = '<div class="cfg"><a href="#">-</a></div>' + ths[a].outerHTML;
|
||||
th.getElementsByTagName('a')[0].onclick = ev_row_tgl;
|
||||
if (ttv) {
|
||||
th.setAttribute("tt", ttv);
|
||||
th.setAttribute("ttd", "u");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2640,7 +2708,10 @@ var filecols = (function () {
|
||||
hcols = ebi('hcols');
|
||||
|
||||
for (var a = 0; a < hidden.length; a++) {
|
||||
html.push('<a href="#" class="btn">' + esc(hidden[a]) + '</a>');
|
||||
var ttv = tts[hidden[a]],
|
||||
tta = ttv ? ' tt="' + ttv + '">' : '>';
|
||||
|
||||
html.push('<a href="#" class="btn"' + tta + esc(hidden[a]) + '</a>');
|
||||
}
|
||||
hcols.previousSibling.style.display = html.length ? 'block' : 'none';
|
||||
hcols.innerHTML = html.join('\n');
|
||||
@@ -2673,6 +2744,10 @@ var filecols = (function () {
|
||||
for (var b = 0, bb = tds.length; b < bb; b++)
|
||||
tds[b].setAttribute('class', cls);
|
||||
}
|
||||
if (window['tt']) {
|
||||
tt.att(ebi('hcols'));
|
||||
tt.att(QS('#files thead'));
|
||||
}
|
||||
};
|
||||
set_style();
|
||||
|
||||
@@ -2863,7 +2938,7 @@ var arcfmt = (function () {
|
||||
["tar", "tar", "plain gnutar file"],
|
||||
["zip", "zip=utf8", "zip with utf8 filenames (maybe wonky on windows 7 and older)"],
|
||||
["zip_dos", "zip", "zip with traditional cp437 filenames, for really old software"],
|
||||
["zip_crc", "zip=crc", "cp437 with crc32 computed early for truly ancient software$N(takes longer to process before download can start)"]
|
||||
["zip_crc", "zip=crc", "cp437 with crc32 computed early,$Nfor MS-DOS PKZIP v2.04g (october 1993)$N(takes longer to process before download can start)"]
|
||||
];
|
||||
|
||||
for (var a = 0; a < fmts.length; a++) {
|
||||
@@ -2895,7 +2970,7 @@ var arcfmt = (function () {
|
||||
|
||||
var ofs = href.lastIndexOf('?');
|
||||
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.textContent = fmt.split('_')[0];
|
||||
@@ -3057,7 +3132,7 @@ function reload_browser(not_mp) {
|
||||
|
||||
var oo = QSA('#files>tbody>tr>td:nth-child(3)');
|
||||
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, " ");
|
||||
|
||||
oo[a].textContent = hsz;
|
||||
|
||||
@@ -8,6 +8,47 @@ html, body {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
#tt {
|
||||
position: fixed;
|
||||
max-width: 34em;
|
||||
background: #222;
|
||||
border: 0 solid #777;
|
||||
overflow: hidden;
|
||||
margin-top: 1em;
|
||||
padding: 0 1.3em;
|
||||
height: 0;
|
||||
opacity: .1;
|
||||
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||
box-shadow: 0 .2em .5em #222;
|
||||
border-radius: .4em;
|
||||
z-index: 9001;
|
||||
}
|
||||
#tt.b {
|
||||
padding: 0 2em;
|
||||
border-radius: .5em;
|
||||
box-shadow: 0 .2em 1em #000;
|
||||
}
|
||||
#tt.show {
|
||||
padding: 1em 1.3em;
|
||||
border-width: .4em 0;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
#tt.show.b {
|
||||
padding: 1.5em 2em;
|
||||
border-width: .5em 0;
|
||||
}
|
||||
#tt code {
|
||||
background: #3c3c3c;
|
||||
padding: .1em .3em;
|
||||
border-top: 1px solid #777;
|
||||
border-radius: .3em;
|
||||
font-family: monospace, monospace;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
#tt em {
|
||||
color: #f6a;
|
||||
}
|
||||
#mtw {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<a id="lightswitch" href="#">go dark</a>
|
||||
<a id="navtoggle" href="#">hide nav</a>
|
||||
{%- if edit %}
|
||||
<a id="save" href="?edit">save</a>
|
||||
<a id="sbs" href="#">sbs</a>
|
||||
<a id="nsbs" href="#">editor</a>
|
||||
<a id="save" href="?edit" tt="Hotkey: ctrl-s">save</a>
|
||||
<a id="sbs" href="#" tt="editor and preview side by side">sbs</a>
|
||||
<a id="nsbs" href="#" tt="switch between editor and preview$NHotkey: ctrl-e">editor</a>
|
||||
<div id="toolsbox">
|
||||
<a id="tools" href="#">tools</a>
|
||||
<a id="fmt_table" href="#">prettify table (ctrl-k)</a>
|
||||
@@ -26,8 +26,8 @@
|
||||
<a id="help" href="#">help</a>
|
||||
</div>
|
||||
{%- else %}
|
||||
<a href="?edit">edit (basic)</a>
|
||||
<a href="?edit2">edit (fancy)</a>
|
||||
<a href="?edit" tt="good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown">edit (basic)</a>
|
||||
<a href="?edit2" tt="not in-house so probably less buggy">edit (fancy)</a>
|
||||
<a href="?raw">view raw</a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@ function md_plug_err(ex, js) {
|
||||
var lns = js.split('\n');
|
||||
if (ln < lns.length) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -530,3 +530,6 @@ dom_navtgl.onclick = function () {
|
||||
|
||||
if (sread('hidenav') == 1)
|
||||
dom_navtgl.onclick();
|
||||
|
||||
if (window['tt'])
|
||||
tt.init();
|
||||
|
||||
@@ -924,10 +924,9 @@ function cfg_uni(e) {
|
||||
(function () {
|
||||
function keydown(ev) {
|
||||
ev = ev || window.event;
|
||||
var kc = ev.keyCode || ev.which;
|
||||
var ctrl = ev.ctrlKey || ev.metaKey;
|
||||
//console.log(ev.code, kc);
|
||||
if (ctrl && (ev.code == "KeyS" || kc == 83)) {
|
||||
var kc = ev.code || ev.keyCode || ev.which;
|
||||
//console.log(ev.key, ev.code, ev.keyCode, ev.which);
|
||||
if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) {
|
||||
save();
|
||||
return false;
|
||||
}
|
||||
@@ -936,23 +935,15 @@ function cfg_uni(e) {
|
||||
if (d)
|
||||
d.click();
|
||||
}
|
||||
if (document.activeElement == dom_src) {
|
||||
if (ev.code == "Tab" || kc == 9) {
|
||||
md_indent(ev.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (ctrl && (ev.code == "KeyH" || kc == 72)) {
|
||||
if (document.activeElement != dom_src)
|
||||
return true;
|
||||
|
||||
if (ctrl(ev)) {
|
||||
if (ev.code == "KeyH" || kc == 72) {
|
||||
md_header(ev.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (!ctrl && (ev.code == "Home" || kc == 36)) {
|
||||
md_home(ev.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (!ctrl && !ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
||||
return md_newline();
|
||||
}
|
||||
if (ctrl && (ev.code == "KeyZ" || kc == 90)) {
|
||||
if (ev.code == "KeyZ" || kc == 90) {
|
||||
if (ev.shiftKey)
|
||||
action_stack.redo();
|
||||
else
|
||||
@@ -960,33 +951,45 @@ function cfg_uni(e) {
|
||||
|
||||
return false;
|
||||
}
|
||||
if (ctrl && (ev.code == "KeyY" || kc == 89)) {
|
||||
if (ev.code == "KeyY" || kc == 89) {
|
||||
action_stack.redo();
|
||||
return false;
|
||||
}
|
||||
if (!ctrl && !ev.shiftKey && kc == 8) {
|
||||
return md_backspace();
|
||||
}
|
||||
if (ctrl && (ev.code == "KeyK")) {
|
||||
if (ev.code == "KeyK") {
|
||||
fmt_table();
|
||||
return false;
|
||||
}
|
||||
if (ctrl && (ev.code == "KeyU")) {
|
||||
if (ev.code == "KeyU") {
|
||||
iter_uni();
|
||||
return false;
|
||||
}
|
||||
if (ctrl && (ev.code == "KeyE")) {
|
||||
if (ev.code == "KeyE") {
|
||||
dom_nsbs.click();
|
||||
//fmt_table();
|
||||
return false;
|
||||
}
|
||||
var up = ev.code == "ArrowUp" || kc == 38;
|
||||
var dn = ev.code == "ArrowDown" || kc == 40;
|
||||
if (ctrl && (up || dn)) {
|
||||
if (up || dn) {
|
||||
md_p_jump(dn);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (ev.code == "Tab" || kc == 9) {
|
||||
md_indent(ev.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "Home" || kc == 36) {
|
||||
md_home(ev.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (!ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
||||
return md_newline();
|
||||
}
|
||||
if (!ev.shiftKey && kc == 8) {
|
||||
return md_backspace();
|
||||
}
|
||||
}
|
||||
}
|
||||
document.onkeydown = keydown;
|
||||
ebi('save').onclick = save;
|
||||
|
||||
@@ -290,8 +290,7 @@ function U2pvis(act, btns) {
|
||||
if (this.is_act(this.tab[a].in))
|
||||
console.log("tab %d/%d = sz %s", a, aa, this.tab[a].bt);
|
||||
|
||||
console.log("a");
|
||||
throw 42;
|
||||
throw new Error('see console');
|
||||
}
|
||||
|
||||
obj.innerHTML = fo.hp;
|
||||
@@ -304,15 +303,8 @@ function U2pvis(act, btns) {
|
||||
oldcat = fo.in,
|
||||
bz_act = this.act == "bz";
|
||||
|
||||
if (oldcat == newcat) {
|
||||
throw 42;
|
||||
}
|
||||
|
||||
//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);
|
||||
if (oldcat == newcat)
|
||||
return;
|
||||
|
||||
fo.in = newcat;
|
||||
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) {
|
||||
// show modal message
|
||||
function showmodal(msg) {
|
||||
@@ -495,8 +496,9 @@ function up2k_init(subtle) {
|
||||
shame = 'your browser is impressively ancient';
|
||||
|
||||
// upload ui hidden by default, clicking the header shows it
|
||||
var got_deps = false;
|
||||
function init_deps() {
|
||||
if (!subtle && !window.asmCrypto) {
|
||||
if (!got_deps && !subtle && !window.asmCrypto) {
|
||||
var fn = 'sha512.' + sha_js + '.js';
|
||||
showmodal('<h1>loading ' + fn + '</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>');
|
||||
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:#' +
|
||||
(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
|
||||
@@ -1298,8 +1301,8 @@ function up2k_init(subtle) {
|
||||
smsg = '';
|
||||
|
||||
if (!response || !response.hits || !response.hits.length) {
|
||||
msg = 'not found on server';
|
||||
smsg = '404';
|
||||
msg = 'not found on server <a href="#" onclick="fsearch_explain()" class="fsearch_explain">(explain)</a>';
|
||||
}
|
||||
else {
|
||||
smsg = 'found';
|
||||
@@ -1570,7 +1573,7 @@ function up2k_init(subtle) {
|
||||
for (var a = o.length - 1; a >= 0; a--) {
|
||||
o[a].parentNode.getElementsByTagName('input')[0].setAttribute('tt', o[a].getAttribute('tt'));
|
||||
}
|
||||
tt.init();
|
||||
tt.att(QS('#u2conf'));
|
||||
|
||||
function bumpthread2(e) {
|
||||
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||
|
||||
@@ -257,6 +257,11 @@ html.light #u2foot .warn span {
|
||||
float: right;
|
||||
margin-bottom: -.3em;
|
||||
}
|
||||
.fsearch_explain {
|
||||
padding-left: .7em;
|
||||
font-size: 1.1em;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ if (!window['console'])
|
||||
|
||||
|
||||
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);
|
||||
|
||||
|
||||
// error handler for mobile devices
|
||||
@@ -27,20 +28,24 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
|
||||
window.onerror = undefined;
|
||||
window['vis_exh'] = null;
|
||||
var html = ['<h1>you hit a bug!</h1><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(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</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>'];
|
||||
|
||||
if (error) {
|
||||
var find = ['desc', 'stack', 'trace'];
|
||||
for (var a = 0; a < find.length; a++)
|
||||
if (String(error[find[a]]) !== 'undefined')
|
||||
html.push('<h2>' + find[a] + '</h2>' +
|
||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
||||
try {
|
||||
if (error) {
|
||||
var find = ['desc', 'stack', 'trace'];
|
||||
for (var a = 0; a < find.length; a++)
|
||||
if (String(error[find[a]]) !== 'undefined')
|
||||
html.push('<h3>' + find[a] + '</h3>' +
|
||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
||||
}
|
||||
html.push('<h3>localStore</h3>' + esc(JSON.stringify(localStorage)));
|
||||
}
|
||||
catch (e) { }
|
||||
document.body.innerHTML = html.join('\n');
|
||||
|
||||
var s = mknod('style');
|
||||
s.innerHTML = 'body{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em} code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} *{line-height:1.5em}';
|
||||
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:1em 0 0 0} code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} *{line-height:1.5em}';
|
||||
document.head.appendChild(s);
|
||||
|
||||
throw 'fatal_err';
|
||||
@@ -53,6 +58,11 @@ var ebi = document.getElementById.bind(document),
|
||||
mknod = document.createElement.bind(document);
|
||||
|
||||
|
||||
function ctrl(e) {
|
||||
return e && (e.ctrlKey || e.metaKey);
|
||||
}
|
||||
|
||||
|
||||
function ev(e) {
|
||||
e = e || window.event;
|
||||
if (!e)
|
||||
@@ -497,13 +507,20 @@ function hist_replace(url) {
|
||||
var tt = (function () {
|
||||
var r = {
|
||||
"tt": mknod("div"),
|
||||
"en": true
|
||||
"en": true,
|
||||
"el": null,
|
||||
"skip": false
|
||||
};
|
||||
|
||||
r.tt.setAttribute('id', 'tt');
|
||||
document.body.appendChild(r.tt);
|
||||
|
||||
function show() {
|
||||
r.show = function () {
|
||||
if (r.skip) {
|
||||
r.skip = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var cfg = sread('tooltips');
|
||||
if (cfg !== null && cfg != '1')
|
||||
return;
|
||||
@@ -512,11 +529,18 @@ var tt = (function () {
|
||||
if (!msg)
|
||||
return;
|
||||
|
||||
r.el = this;
|
||||
var pos = this.getBoundingClientRect(),
|
||||
dir = this.getAttribute('ttd') || '',
|
||||
left = pos.left < window.innerWidth / 2,
|
||||
top = pos.top < window.innerHeight / 2,
|
||||
big = this.className.indexOf(' ttb') !== -1;
|
||||
|
||||
if (dir.indexOf('u') + 1) top = false;
|
||||
if (dir.indexOf('d') + 1) top = true;
|
||||
if (dir.indexOf('l') + 1) left = false;
|
||||
if (dir.indexOf('r') + 1) left = true;
|
||||
|
||||
clmod(r.tt, 'b', big);
|
||||
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
|
||||
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
|
||||
@@ -524,11 +548,43 @@ var tt = (function () {
|
||||
r.tt.style.right = left ? 'auto' : (window.innerWidth - pos.right) + 'px';
|
||||
|
||||
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
||||
r.el.addEventListener('mouseleave', r.hide);
|
||||
clmod(r.tt, 'show', 1);
|
||||
};
|
||||
|
||||
r.hide = function (e) {
|
||||
ev(e);
|
||||
clmod(r.tt, 'show');
|
||||
if (r.el)
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clmod(r.tt, 'show');
|
||||
r.tt.onclick = r.hide;
|
||||
|
||||
r.att = function (ctr) {
|
||||
var _show = r.en ? r.show : null,
|
||||
_hide = r.en ? r.hide : null,
|
||||
o = ctr.querySelectorAll('*[tt]');
|
||||
|
||||
for (var a = o.length - 1; a >= 0; a--) {
|
||||
o[a].onfocus = _show;
|
||||
o[a].onblur = _hide;
|
||||
o[a].onmouseenter = _show;
|
||||
o[a].onmouseleave = _hide;
|
||||
}
|
||||
r.hide();
|
||||
}
|
||||
|
||||
r.init = function () {
|
||||
@@ -542,18 +598,7 @@ var tt = (function () {
|
||||
};
|
||||
r.en = bcfg_get('tooltips', true)
|
||||
}
|
||||
|
||||
var _show = r.en ? show : null,
|
||||
_hide = r.en ? hide : null;
|
||||
|
||||
var o = QSA('*[tt]');
|
||||
for (var a = o.length - 1; a >= 0; a--) {
|
||||
o[a].onfocus = _show;
|
||||
o[a].onblur = _hide;
|
||||
o[a].onmouseenter = _show;
|
||||
o[a].onmouseleave = _hide;
|
||||
}
|
||||
hide();
|
||||
r.att(document);
|
||||
};
|
||||
|
||||
return r;
|
||||
|
||||
@@ -166,7 +166,10 @@ dbg.asyncStore.pendingBreakpoints = {}
|
||||
about:config >> devtools.debugger.prefs-schema-version = -1
|
||||
|
||||
# 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
|
||||
# (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
|
||||
@@ -57,14 +62,18 @@ use_gz=
|
||||
do_sh=1
|
||||
do_py=1
|
||||
while [ ! -z "$1" ]; do
|
||||
[ "$1" = clean ] && clean=1 && shift && continue
|
||||
[ "$1" = re ] && repack=1 && shift && continue
|
||||
[ "$1" = gz ] && use_gz=1 && shift && continue
|
||||
[ "$1" = no-ogv ] && no_ogv=1 && shift && continue
|
||||
[ "$1" = no-cm ] && no_cm=1 && shift && continue
|
||||
[ "$1" = no-sh ] && do_sh= && shift && continue
|
||||
[ "$1" = no-py ] && do_py= && shift && continue
|
||||
break
|
||||
case $1 in
|
||||
clean) clean=1 ; ;;
|
||||
re) repack=1 ; ;;
|
||||
gz) use_gz=1 ; ;;
|
||||
no-ogv) no_ogv=1 ; ;;
|
||||
no-fnt) no_fnt=1 ; ;;
|
||||
no-dd) no_dd=1 ; ;;
|
||||
no-cm) no_cm=1 ; ;;
|
||||
no-sh) do_sh= ; ;;
|
||||
no-py) do_py= ; ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
tmv() {
|
||||
@@ -190,6 +199,18 @@ done
|
||||
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 ] ||
|
||||
find | grep -E '\.py$' |
|
||||
grep -vE '__version__' |
|
||||
|
||||
@@ -6,8 +6,8 @@ import re, os, sys, time, shutil, signal, threading, tarfile, hashlib, platform,
|
||||
import subprocess as sp
|
||||
|
||||
"""
|
||||
pls don't edit this file with a text editor,
|
||||
it breaks the compressed stuff at the end
|
||||
to edit this file, use HxD or "vim -b"
|
||||
(there is compressed stuff at the end)
|
||||
|
||||
run me with any version of python, i will unpack and run copyparty
|
||||
|
||||
@@ -380,7 +380,7 @@ def run(tmp, j2):
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except Exception as ex:
|
||||
if not WINDOWS:
|
||||
msg("\033[31mflock:", repr(ex))
|
||||
msg("\033[31mflock:{!r}\033[0m".format(ex))
|
||||
|
||||
t = threading.Thread(target=utime, args=(tmp,))
|
||||
t.daemon = True
|
||||
|
||||
@@ -23,10 +23,10 @@ def hdr(query):
|
||||
|
||||
|
||||
class Cfg(Namespace):
|
||||
def __init__(self, a=[], v=[], c=None):
|
||||
def __init__(self, a=None, v=None, c=None):
|
||||
super(Cfg, self).__init__(
|
||||
a=a,
|
||||
v=v,
|
||||
a=a or [],
|
||||
v=v or [],
|
||||
c=c,
|
||||
rproxy=0,
|
||||
ed=False,
|
||||
|
||||
@@ -16,7 +16,7 @@ from copyparty import util
|
||||
|
||||
|
||||
class Cfg(Namespace):
|
||||
def __init__(self, a=[], v=[], c=None):
|
||||
def __init__(self, a=None, v=None, c=None):
|
||||
ex = {k: False for k in "nw e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
||||
ex2 = {
|
||||
"mtp": [],
|
||||
@@ -27,7 +27,7 @@ class Cfg(Namespace):
|
||||
"rproxy": 0,
|
||||
}
|
||||
ex.update(ex2)
|
||||
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)
|
||||
super(Cfg, self).__init__(a=a or [], v=v or [], c=c, **ex)
|
||||
|
||||
|
||||
class TestVFS(unittest.TestCase):
|
||||
|
||||
@@ -66,6 +66,14 @@ def get_ramdisk():
|
||||
for _ in range(10):
|
||||
try:
|
||||
_, _ = chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname)
|
||||
with open("/Volumes/cptd/.metadata_never_index", "w") as f:
|
||||
f.write("orz")
|
||||
|
||||
try:
|
||||
shutil.rmtree("/Volumes/cptd/.fseventsd")
|
||||
except:
|
||||
pass
|
||||
|
||||
return subdir("/Volumes/cptd")
|
||||
except Exception as ex:
|
||||
print(repr(ex))
|
||||
@@ -119,14 +127,13 @@ class VHttpConn(object):
|
||||
self.addr = ("127.0.0.1", "42069")
|
||||
self.args = args
|
||||
self.asrv = asrv
|
||||
self.is_mp = False
|
||||
self.nid = None
|
||||
self.log_func = log
|
||||
self.log_src = "a"
|
||||
self.lf_url = None
|
||||
self.hsrv = VHttpSrv()
|
||||
self.nreq = 0
|
||||
self.nbyte = 0
|
||||
self.workload = 0
|
||||
self.ico = None
|
||||
self.thumbcli = None
|
||||
self.t0 = time.time()
|
||||
Reference in New Issue
Block a user