Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cade1990ce | ||
|
|
59b6e61816 | ||
|
|
daff7ff158 | ||
|
|
0862860961 | ||
|
|
1cb24045a0 | ||
|
|
622358b172 | ||
|
|
7998884a9d | ||
|
|
51ddecd101 | ||
|
|
7a35ab1d1e | ||
|
|
48564ba52a | ||
|
|
49efffd740 | ||
|
|
d6ac224c8f | ||
|
|
a772b8c3f2 | ||
|
|
b580953dcd | ||
|
|
d86653c763 | ||
|
|
dded4fca76 | ||
|
|
36365ffa6b | ||
|
|
0f9aeeaa27 | ||
|
|
d8ebcd0ef7 | ||
|
|
6e445487b1 | ||
|
|
6605e461c7 | ||
|
|
40ce4e2275 | ||
|
|
8fef9e363e | ||
|
|
4792c2770d | ||
|
|
87bb49da36 | ||
|
|
1c0071d9ce | ||
|
|
efded35c2e | ||
|
|
1d74240b9a | ||
|
|
098184ff7b | ||
|
|
4083533916 | ||
|
|
feb1acd43a | ||
|
|
a9591db734 | ||
|
|
9ebf148cbe | ||
|
|
a473e5e19a | ||
|
|
5d3034c231 | ||
|
|
c3a895af64 | ||
|
|
cea5aecbf2 | ||
|
|
0e61e70670 | ||
|
|
1e333c0939 | ||
|
|
917b6ec03c | ||
|
|
fe67c52ead | ||
|
|
909c7bee3e | ||
|
|
27ca54d138 | ||
|
|
2147c3a646 | ||
|
|
a99120116f | ||
|
|
802efeaff2 | ||
|
|
9ad3af1ef6 | ||
|
|
715727b811 | ||
|
|
c6eaa7b836 |
115
README.md
115
README.md
@@ -9,11 +9,12 @@
|
||||
turn your phone or raspi into a portable file server with resumable uploads/downloads using *any* web browser
|
||||
|
||||
* server only needs `py2.7` or `py3.3+`, all dependencies optional
|
||||
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
|
||||
* *resumable* uploads need `firefox 34+` / `chrome 41+` / `safari 7+` for full speed
|
||||
* code standard: `black`
|
||||
* browse/upload with [IE4](#browser-support) / netscape4.0 on win3.11 (heh)
|
||||
* *resumable* uploads need `firefox 34+` / `chrome 41+` / `safari 7+`
|
||||
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer) // [ie4](#browser-support)
|
||||
try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
|
||||
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
|
||||
|
||||
|
||||
## get the app
|
||||
@@ -43,7 +44,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
* [tabs](#tabs) - the main tabs in the ui
|
||||
* [hotkeys](#hotkeys) - the browser has the following hotkeys
|
||||
* [navpane](#navpane) - switching between breadcrumbs or navpane
|
||||
* [thumbnails](#thumbnails) - press `g` to toggle grid-view instead of the file listing
|
||||
* [thumbnails](#thumbnails) - press `g` or `田` to toggle grid-view instead of the file listing
|
||||
* [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files
|
||||
* [uploading](#uploading) - drag files/folders into the web-browser to upload
|
||||
* [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server
|
||||
@@ -101,7 +102,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
|
||||
download **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** and you're all set!
|
||||
|
||||
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; see `-h` for help if you want [accounts and volumes](#accounts-and-volumes) etc
|
||||
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes)
|
||||
|
||||
some recommended options:
|
||||
* `-e2dsa` enables general [file indexing](#file-indexing)
|
||||
@@ -109,7 +110,7 @@ some recommended options:
|
||||
* `-v /mnt/music:/music:r:rw,foo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, and read-write for user `foo`, password `bar`
|
||||
* replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else
|
||||
* see [accounts and volumes](#accounts-and-volumes) for the syntax and other permissions (`r`ead, `w`rite, `m`ove, `d`elete, `g`et)
|
||||
* `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access
|
||||
* `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access (see `--help-ls`)
|
||||
|
||||
|
||||
### on servers
|
||||
@@ -167,7 +168,7 @@ feature summary
|
||||
* download
|
||||
* ☑ single files in browser
|
||||
* ☑ [folders as zip / tar files](#zip-downloads)
|
||||
* ☑ FUSE client (read-only)
|
||||
* ☑ [FUSE client](https://github.com/9001/copyparty/tree/hovudstraum/bin#copyparty-fusepy) (read-only)
|
||||
* browser
|
||||
* ☑ [navpane](#navpane) (directory tree sidebar)
|
||||
* ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))
|
||||
@@ -203,6 +204,7 @@ project goals / philosophy
|
||||
* inverse linux philosophy -- do all the things, and do an *okay* job
|
||||
* quick drop-in service to get a lot of features in a pinch
|
||||
* there are probably [better alternatives](https://github.com/awesome-selfhosted/awesome-selfhosted) if you have specific/long-term needs
|
||||
* but the resumable multithreaded uploads are p slick ngl
|
||||
* run anywhere, support everything
|
||||
* as many web-browsers and python versions as possible
|
||||
* every browser should at least be able to browse, download, upload files
|
||||
@@ -241,7 +243,7 @@ some improvement ideas
|
||||
|
||||
## general bugs
|
||||
|
||||
* Windows: if the up2k db is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit
|
||||
* Windows: if the `up2k.db` (filesystem index) is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit
|
||||
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db on a local disk instead
|
||||
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
||||
* probably more, pls let me know
|
||||
@@ -273,7 +275,7 @@ some improvement ideas
|
||||
* you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty
|
||||
|
||||
* can I make copyparty download a file to my server if I give it a URL?
|
||||
* not officially, but there is a [terrible hack](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/wget.py) which makes it possible
|
||||
* not really, but there is a [terrible hack](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/wget.py) which makes it possible
|
||||
|
||||
|
||||
# accounts and volumes
|
||||
@@ -281,6 +283,8 @@ some improvement ideas
|
||||
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
|
||||
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if logged in as admin)
|
||||
|
||||
a quick summary can be seen using `--help-accounts`
|
||||
|
||||
configuring accounts/volumes with arguments:
|
||||
* `-a usr:pwd` adds account `usr` with password `pwd`
|
||||
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
|
||||
@@ -337,7 +341,7 @@ the browser has the following hotkeys (always qwerty)
|
||||
* `I/K` prev/next folder
|
||||
* `M` parent folder (or unexpand current)
|
||||
* `V` toggle folders / textfiles in the navpane
|
||||
* `G` toggle list / [grid view](#thumbnails)
|
||||
* `G` toggle list / [grid view](#thumbnails) -- same as `田` bottom-right
|
||||
* `T` toggle thumbnails / icons
|
||||
* `ESC` close various things
|
||||
* `ctrl-X` cut selected files/folders
|
||||
@@ -358,19 +362,23 @@ the browser has the following hotkeys (always qwerty)
|
||||
* `U/O` skip 10sec back/forward
|
||||
* `0..9` jump to 0%..90%
|
||||
* `P` play/pause (also starts playing the folder)
|
||||
* `Y` download file
|
||||
* when viewing images / playing videos:
|
||||
* `J/L, Left/Right` prev/next file
|
||||
* `Home/End` first/last file
|
||||
* `F` toggle fullscreen
|
||||
* `S` toggle selection
|
||||
* `R` rotate clockwise (shift=ccw)
|
||||
* `Y` download file
|
||||
* `Esc` close viewer
|
||||
* videos:
|
||||
* `U/O` skip 10sec back/forward
|
||||
* `P/K/Space` play/pause
|
||||
* `F` fullscreen
|
||||
* `C` continue playing next video
|
||||
* `V` loop
|
||||
* `M` mute
|
||||
* `C` continue playing next video
|
||||
* `V` loop entire file
|
||||
* `[` loop range (start)
|
||||
* `]` loop range (end)
|
||||
* when the navpane is open:
|
||||
* `A/D` adjust tree width
|
||||
* in the [grid view](#thumbnails):
|
||||
@@ -402,7 +410,7 @@ click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (
|
||||
|
||||
## thumbnails
|
||||
|
||||
press `g` to toggle grid-view instead of the file listing, and `t` toggles icons / thumbnails
|
||||
press `g` or `田` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails
|
||||
|
||||

|
||||
|
||||
@@ -444,13 +452,13 @@ you can also zip a selection of files or folders by clicking them in the browser
|
||||
|
||||
## uploading
|
||||
|
||||
drag files/folders into the web-browser to upload
|
||||
drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy))
|
||||
|
||||
this initiates an upload using `up2k`; there are two uploaders available:
|
||||
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||
* `[🚀] up2k`, the fancy one
|
||||
* `[🚀] up2k`, the good / fancy one
|
||||
|
||||
you can also undo/delete uploads by using `[🧯]` [unpost](#unpost)
|
||||
NB: you can undo/delete your own uploads with `[🧯]` [unpost](#unpost)
|
||||
|
||||
up2k has several advantages:
|
||||
* you can drop folders into the browser (files are added recursively)
|
||||
@@ -462,7 +470,7 @@ up2k has several advantages:
|
||||
* 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
|
||||
see [up2k](#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
|
||||
|
||||

|
||||
|
||||
@@ -474,7 +482,6 @@ the up2k UI is the epitome of polished inutitive experiences:
|
||||
* "parallel uploads" specifies how many chunks to upload at the same time
|
||||
* `[🏃]` analysis of other files should continue while one is uploading
|
||||
* `[💭]` ask for confirmation before files are added to the queue
|
||||
* `[💤]` sync uploading between other copyparty browser-tabs so only one is active
|
||||
* `[🔎]` switch between upload and [file-search](#file-search) mode
|
||||
* ignore `[🔎]` if you add files by dragging them into the browser
|
||||
|
||||
@@ -486,7 +493,7 @@ and then theres the tabs below it,
|
||||
* plus up to 3 entries each from `[done]` and `[que]` for context
|
||||
* `[que]` is all the files that are still queued
|
||||
|
||||
note that since up2k has to read each file twice, `[🎈 bup]` can *theoretically* be up to 2x faster in some extreme cases (files bigger than your ram, combined with an internet connection faster than the read-speed of your HDD, or if you're uploading from a cuo2duo)
|
||||
note that since up2k has to read each file twice, `[🎈] bup` can *theoretically* be up to 2x faster in some extreme cases (files bigger than your ram, combined with an internet connection faster than the read-speed of your HDD, or if you're uploading from a cuo2duo)
|
||||
|
||||
if you are resuming a massive upload and want to skip hashing the files which already finished, you can enable `turbo` in the `[⚙️] config` tab, but please read the tooltip on that button
|
||||
|
||||
@@ -597,7 +604,7 @@ and there are *two* editors
|
||||
|
||||
* get a plaintext file listing by adding `?ls=t` to a URL, or a compact colored one with `?ls=v` (for unix terminals)
|
||||
|
||||
* if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider https://ocv.me/dev/?media-osd-bgone.ps1
|
||||
* if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider [./contrib/media-osd-bgone.ps1](contrib/#media-osd-bgoneps1)
|
||||
|
||||
* click the bottom-left `π` to open a javascript prompt for debugging
|
||||
|
||||
@@ -620,7 +627,9 @@ path/name queries are space-separated, AND'ed together, and words are negated wi
|
||||
* path: `shibayan -bossa` finds all files where one of the folders contain `shibayan` but filters out any results where `bossa` exists somewhere in the path
|
||||
* name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9)
|
||||
|
||||
add the argument `-e2ts` to also scan/index tags from music files, which brings us over to:
|
||||
the `raw` field allows for more complex stuff such as `( tags like *nhato* or tags like *taishi* ) and ( not tags like *nhato* or not tags like *taishi* )` which finds all songs by either nhato or taishi, excluding collabs (terrible example, why would you do that)
|
||||
|
||||
for the above example to work, add the commandline argument `-e2ts` to also scan/index tags from music files, which brings us over to:
|
||||
|
||||
|
||||
# server config
|
||||
@@ -767,27 +776,32 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy
|
||||
|
||||
provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md)
|
||||
|
||||
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
|
||||
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, and only files which contain audio get analyzed by default (see ay/an/ad below)
|
||||
|
||||
* `-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,`)
|
||||
* `-v ~/music::r:c,mtp=.bpm=~/bin/audio-bpm.py:c,mtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly
|
||||
|
||||
*but wait, there's more!* `-mtp` can be used for non-audio files as well using the `a` flag: `ay` only do audio files, `an` only do non-audio files, or `ad` do all files (d as in dontcare)
|
||||
*but wait, there's more!* `-mtp` can be used for non-audio files as well using the `a` flag: `ay` only do audio files (default), `an` only do non-audio files, or `ad` do all files (d as in dontcare)
|
||||
|
||||
* "audio file" also means videos btw, as long as there is an audio stream
|
||||
* `-mtp ext=an,~/bin/file-ext.py` runs `~/bin/file-ext.py` to get the `ext` tag only if file is not audio (`an`)
|
||||
* `-mtp arch,built,ver,orig=an,eexe,edll,~/bin/exe.py` runs `~/bin/exe.py` to get properties about windows-binaries only if file is not audio (`an`) and file extension is exe or dll
|
||||
|
||||
you can control how the parser is killed if it times out with option `kt` killing the entire process tree (default), `km` just the main process, or `kn` let it continue running until copyparty is terminated
|
||||
|
||||
if something doesn't work, try `--mtag-v` for verbose error messages
|
||||
|
||||
|
||||
## upload events
|
||||
|
||||
trigger a script/program on each upload like so:
|
||||
|
||||
```
|
||||
-v /mnt/inc:inc:w:c,mte=+a1:c,mtp=a1=ad,/usr/bin/notify-send
|
||||
-v /mnt/inc:inc:w:c,mte=+x1:c,mtp=x1=ad,kn,/usr/bin/notify-send
|
||||
```
|
||||
|
||||
so filesystem location `/mnt/inc` shared at `/inc`, write-only for everyone, appending `a1` to the list of tags to index, and using `/usr/bin/notify-send` to "provide" that tag
|
||||
so filesystem location `/mnt/inc` shared at `/inc`, write-only for everyone, appending `x1` to the list of tags to index (`mte`), and using `/usr/bin/notify-send` to "provide" tag `x1` for any filetype (`ad`) with kill-on-timeout disabled (`kn`)
|
||||
|
||||
that'll run the command `notify-send` with the path to the uploaded file as the first and only argument (so on linux it'll show a notification on-screen)
|
||||
|
||||
@@ -834,8 +848,17 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
|
||||
|
||||
## complete examples
|
||||
|
||||
* read-only music server with bpm and key scanning
|
||||
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts -mtp .bpm=f,audio-bpm.py -mtp key=f,audio-key.py`
|
||||
* read-only music server
|
||||
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
|
||||
|
||||
* ...with bpm and key scanning
|
||||
`-mtp .bpm=f,audio-bpm.py -mtp key=f,audio-key.py`
|
||||
|
||||
* ...with a read-write folder for `kevin` whose password is `okgo`
|
||||
`-a kevin:okgo -v /mnt/nas/inc:/inc:rw,kevin`
|
||||
|
||||
* ...with logging to disk
|
||||
`-lo log/cpp-%Y-%m%d-%H%M%S.txt.xz`
|
||||
|
||||
|
||||
# browser support
|
||||
@@ -882,6 +905,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
||||
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
|
||||
| **opera** (11.60/winxp) | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio |
|
||||
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
|
||||
| **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
|
||||
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
|
||||
|
||||
|
||||
@@ -945,12 +969,14 @@ up2k has saved a few uploads from becoming corrupted in-transfer already; caught
|
||||
|
||||
a single sha512 would be better, right?
|
||||
|
||||
this is due to `crypto.subtle` not providing a streaming api (or the option to seed the sha512 hasher with a starting hash)
|
||||
this is due to `crypto.subtle` [not yet](https://github.com/w3c/webcrypto/issues/73) providing a streaming api (or the option to seed the sha512 hasher with a starting hash)
|
||||
|
||||
as a result, the hashes are much less useful than they could have been (search the server by sha512, provide the sha512 in the response http headers, ...)
|
||||
|
||||
hashwasm would solve the streaming issue but reduces hashing speed for sha512 (xxh128 does 6 GiB/s), and it would make old browsers and [iphones](https://bugs.webkit.org/show_bug.cgi?id=228552) unsupported
|
||||
|
||||
* blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids
|
||||
|
||||
|
||||
# performance
|
||||
|
||||
@@ -988,13 +1014,25 @@ when uploading files,
|
||||
|
||||
some notes on hardening
|
||||
|
||||
on public copyparty instances with anonymous upload enabled:
|
||||
* option `-s` is a shortcut to set the following options:
|
||||
* `--no-thumb` disables thumbnails and audio transcoding to stop copyparty from running `FFmpeg`/`Pillow`/`VIPS` on uploaded files, which is a [good idea](https://www.cvedetails.com/vulnerability-list.php?vendor_id=3611) if anonymous upload is enabled
|
||||
* `--no-mtag-ff` uses `mutagen` to grab music tags instead of `FFmpeg`, which is safer and faster but less accurate
|
||||
* `--dotpart` hides uploads from directory listings while they're still incoming
|
||||
* `--no-robots` and `--force-js` makes life harder for crawlers, see [hiding from google](#hiding-from-google)
|
||||
|
||||
* users can upload html/css/js which will evaluate for other visitors in a few ways,
|
||||
* unless `--no-readme` is set: by uploading/modifying a file named `readme.md`
|
||||
* if `move` access is granted AND none of `--no-logues`, `--no-dot-mv`, `--no-dot-ren` is set: by uploading some .html file and renaming it to `.epilogue.html` (uploading it directly is blocked)
|
||||
* option `-ss` is a shortcut for the above plus:
|
||||
* `--no-logues` and `--no-readme` disables support for readme's and prologues / epilogues in directory listings, which otherwise lets people upload arbitrary `<script>` tags
|
||||
* `--unpost 0`, `--no-del`, `--no-mv` disables all move/delete support
|
||||
* `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance
|
||||
* however note if you edit one file it will also affect the other copies
|
||||
* `--vague-403` returns a "404 not found" instead of "403 forbidden" which is a common enterprise meme
|
||||
* `--nih` removes the server hostname from directory listings
|
||||
|
||||
other misc:
|
||||
* option `-sss` is a shortcut for the above plus:
|
||||
* `-lo cpp-%Y-%m%d-%H%M%S.txt.xz` enables logging to disk
|
||||
* `-ls **,*,ln,p,r` does a scan on startup for any dangerous symlinks
|
||||
|
||||
other misc notes:
|
||||
|
||||
* you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files
|
||||
* combine this with volume-flag `c,fk` to generate per-file accesskeys; users which have full read-access will then see URLs with `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
|
||||
@@ -1242,10 +1280,7 @@ also builds the sfx so skip the sfx section above
|
||||
in the `scripts` folder:
|
||||
|
||||
* run `make -C deps-docker` to build all dependencies
|
||||
* `git tag v1.2.3 && git push origin --tags`
|
||||
* upload to pypi with `make-pypi-release.(sh|bat)`
|
||||
* create github release with `make-tgz-release.sh`
|
||||
* create sfx with `make-sfx.sh`
|
||||
* run `./rls.sh 1.2.3` which uploads to pypi + creates github release + sfx
|
||||
|
||||
|
||||
# todo
|
||||
@@ -1272,7 +1307,7 @@ roughly sorted by priority
|
||||
* up2k partials ui
|
||||
* feels like there isn't much point
|
||||
* cache sha512 chunks on client
|
||||
* too dangerous
|
||||
* too dangerous -- overtaken by turbo mode
|
||||
* comment field
|
||||
* nah
|
||||
* look into android thumbnail cache file format
|
||||
|
||||
@@ -17,7 +17,7 @@ except:
|
||||
|
||||
"""
|
||||
calculates various checksums for uploads,
|
||||
usage: -mtp crc32,md5,sha1,sha256b=bin/mtag/cksum.py
|
||||
usage: -mtp crc32,md5,sha1,sha256b=ad,bin/mtag/cksum.py
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ goes without saying, but this is HELLA DANGEROUS,
|
||||
GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS
|
||||
|
||||
example copyparty config to use this:
|
||||
--urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,bin/mtag/very-bad-idea.py
|
||||
--urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,bin/mtag/very-bad-idea.py
|
||||
|
||||
recommended deps:
|
||||
apt install xdotool libnotify-bin
|
||||
@@ -63,8 +63,8 @@ set -e
|
||||
EOF
|
||||
chmod 755 /usr/local/bin/chromium-browser
|
||||
|
||||
# start the server (note: replace `-v.::rw:` with `-v.::r:` to disallow retrieving uploaded stuff)
|
||||
cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,very-bad-idea.py
|
||||
# start the server (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff)
|
||||
cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
|
||||
|
||||
"""
|
||||
|
||||
|
||||
65
bin/mtag/vidchk.py
Executable file
65
bin/mtag/vidchk.py
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import subprocess as sp
|
||||
|
||||
from copyparty.util import fsenc
|
||||
from copyparty.mtag import ffprobe
|
||||
|
||||
|
||||
"""
|
||||
inspects video files for errors and such
|
||||
usage: -mtp vidchk=t600,ay,bin/mtag/vidchk.py
|
||||
"""
|
||||
|
||||
|
||||
FAST = True # parse entire file at container level
|
||||
# FAST = False # fully decode audio and video streams
|
||||
|
||||
|
||||
def main():
|
||||
fp = sys.argv[1]
|
||||
md, _ = ffprobe(fp)
|
||||
|
||||
try:
|
||||
w = int(md[".resw"][1])
|
||||
h = int(md[".resh"][1])
|
||||
if not w + h:
|
||||
raise Exception()
|
||||
except:
|
||||
return "could not determine resolution"
|
||||
|
||||
if min(w, h) < 720:
|
||||
return "resolution too small"
|
||||
|
||||
zs = (
|
||||
"ffmpeg -y -hide_banner -nostdin -v warning"
|
||||
+ " -err_detect +crccheck+bitstream+buffer+careful+compliant+aggressive+explode"
|
||||
" -xerror -i"
|
||||
)
|
||||
|
||||
cmd = zs.encode("ascii").split(b" ") + [fsenc(fp)]
|
||||
|
||||
if FAST:
|
||||
zs = "-c copy -f null -"
|
||||
else:
|
||||
zs = "-vcodec rawvideo -acodec pcm_s16le -f null -"
|
||||
|
||||
cmd += zs.encode("ascii").split(b" ")
|
||||
|
||||
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||
so, se = p.communicate()
|
||||
rc = p.returncode
|
||||
if rc:
|
||||
err = (so + se).decode("utf-8", "replace").split("\n", 1)[0]
|
||||
return f"ERROR {rc}: {err}"
|
||||
|
||||
if se:
|
||||
err = se.decode("utf-8", "replace").split("\n", 1)[0]
|
||||
return f"Warning: {err}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(main() or "ok")
|
||||
@@ -22,6 +22,9 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share
|
||||
* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$`
|
||||
* `pw`: password (remove `Parameters` if anon-write)
|
||||
|
||||
### [`media-osd-bgone.ps1`](media-osd-bgone.ps1)
|
||||
* disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really
|
||||
|
||||
### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg)
|
||||
* disables thumbnails and folder-type detection in windows explorer
|
||||
* makes it way faster (especially for slow/networked locations (such as copyparty-fuse))
|
||||
|
||||
104
contrib/media-osd-bgone.ps1
Normal file
104
contrib/media-osd-bgone.ps1
Normal file
@@ -0,0 +1,104 @@
|
||||
# media-osd-bgone.ps1: disable media-control OSD on win10do
|
||||
# v1.1, 2021-06-25, ed <irc.rizon.net>, MIT-licensed
|
||||
# https://github.com/9001/copyparty/blob/hovudstraum/contrib/media-osd-bgone.ps1
|
||||
#
|
||||
# locates the first window that looks like the media OSD and minimizes it;
|
||||
# doing this once after each reboot should do the trick
|
||||
# (adjust the width/height filter if it doesn't work)
|
||||
#
|
||||
# ---------------------------------------------------------------------
|
||||
#
|
||||
# tip: save the following as "media-osd-bgone.bat" next to this script:
|
||||
# start cmd /c "powershell -command ""set-executionpolicy -scope process bypass; .\media-osd-bgone.ps1"" & ping -n 2 127.1 >nul"
|
||||
#
|
||||
# then create a shortcut to that bat-file and move the shortcut here:
|
||||
# %appdata%\Microsoft\Windows\Start Menu\Programs\Startup
|
||||
#
|
||||
# and now this will autorun on bootup
|
||||
|
||||
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace A {
|
||||
public class B : Control {
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
|
||||
|
||||
[DllImport("user32.dll", SetLastError=true)]
|
||||
static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT {
|
||||
public int x;
|
||||
public int y;
|
||||
public int x2;
|
||||
public int y2;
|
||||
}
|
||||
|
||||
bool fa() {
|
||||
RECT r;
|
||||
IntPtr it = IntPtr.Zero;
|
||||
while ((it = FindWindowEx(IntPtr.Zero, it, "NativeHWNDHost", "")) != IntPtr.Zero) {
|
||||
if (FindWindowEx(it, IntPtr.Zero, "DirectUIHWND", "") == IntPtr.Zero)
|
||||
continue;
|
||||
|
||||
if (!GetWindowRect(it, out r))
|
||||
continue;
|
||||
|
||||
int w = r.x2 - r.x + 1;
|
||||
int h = r.y2 - r.y + 1;
|
||||
|
||||
Console.WriteLine("[*] hwnd {0:x} @ {1}x{2} sz {3}x{4}", it, r.x, r.y, w, h);
|
||||
if (h != 141)
|
||||
continue;
|
||||
|
||||
ShowWindow(it, 6);
|
||||
Console.WriteLine("[+] poof");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void fb() {
|
||||
keybd_event((byte)Keys.VolumeMute, 0, 0, 0);
|
||||
keybd_event((byte)Keys.VolumeMute, 0, 2, 0);
|
||||
Thread.Sleep(500);
|
||||
keybd_event((byte)Keys.VolumeMute, 0, 0, 0);
|
||||
keybd_event((byte)Keys.VolumeMute, 0, 2, 0);
|
||||
|
||||
while (true) {
|
||||
if (fa()) {
|
||||
break;
|
||||
}
|
||||
Console.WriteLine("[!] not found");
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
this.Invoke((MethodInvoker)delegate {
|
||||
Application.Exit();
|
||||
});
|
||||
}
|
||||
|
||||
public void Run() {
|
||||
Console.WriteLine("[+] hi");
|
||||
new Thread(new ThreadStart(fb)).Start();
|
||||
Application.Run();
|
||||
Console.WriteLine("[+] bye");
|
||||
}
|
||||
}
|
||||
}
|
||||
"@ -ReferencedAssemblies System.Windows.Forms
|
||||
|
||||
(New-Object -TypeName A.B).Run()
|
||||
@@ -11,6 +11,13 @@ save one of these as `.epilogue.html` inside a folder to customize it:
|
||||
|
||||
|
||||
|
||||
## example browser-js
|
||||
point `--js-browser` to one of these by URL:
|
||||
|
||||
* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders
|
||||
|
||||
|
||||
|
||||
## example browser-css
|
||||
point `--css-browser` to one of these by URL:
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
/* make the up2k ui REALLY minimal by hiding a bunch of stuff: */
|
||||
|
||||
#ops, #tree, #path, #wrap>h2:last-child, /* main tabs and navigators (tree/breadcrumbs) */
|
||||
#ops, #tree, #path, #epi+h2, /* main tabs and navigators (tree/breadcrumbs) */
|
||||
|
||||
#u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
||||
|
||||
|
||||
59
contrib/plugins/minimal-up2k.js
Normal file
59
contrib/plugins/minimal-up2k.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
|
||||
makes the up2k ui REALLY minimal by hiding a bunch of stuff
|
||||
|
||||
almost the same as minimal-up2k.html except this one...:
|
||||
|
||||
-- applies to every write-only folder when used with --js-browser
|
||||
|
||||
-- only applies if javascript is enabled
|
||||
|
||||
-- doesn't hide the total upload ETA display
|
||||
|
||||
-- looks slightly better
|
||||
|
||||
*/
|
||||
|
||||
var u2min = `
|
||||
<style>
|
||||
|
||||
#ops, #path, #tree, #files, #epi+div+h2,
|
||||
#u2conf td.c+.c, #u2cards, #u2foot, #srch_dz, #srch_zd {
|
||||
display: none !important;
|
||||
}
|
||||
#u2conf {margin:5em auto 0 auto !important}
|
||||
#u2conf.ww {width:70em}
|
||||
#u2conf.w {width:50em}
|
||||
#u2conf.w .c,
|
||||
#u2conf.w #u2btn_cw {text-align:left}
|
||||
#u2conf.w #u2btn_cw {width:70%}
|
||||
#u2etaw {margin:3em auto}
|
||||
#u2etaw.w {
|
||||
text-align: center;
|
||||
margin: -3.5em auto 5em auto;
|
||||
}
|
||||
#u2etaw.w #u2etas {margin-right:-37em}
|
||||
#u2etaw.w #u2etas.o {margin-top:-2.2em}
|
||||
#u2etaw.ww {margin:-1em auto}
|
||||
#u2etaw.ww #u2etas {padding-left:4em}
|
||||
#u2etas {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
#wrap {margin-left:2em !important}
|
||||
.logue {
|
||||
border: none !important;
|
||||
margin: 2em auto !important;
|
||||
}
|
||||
.logue:before {content:'' !important}
|
||||
|
||||
</style>
|
||||
|
||||
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
||||
`;
|
||||
|
||||
if (!has(perms, 'read')) {
|
||||
var e2 = mknod('div');
|
||||
e2.innerHTML = u2min;
|
||||
ebi('wrap').insertBefore(e2, QS('#epi+h2'));
|
||||
}
|
||||
@@ -311,7 +311,7 @@ def disable_quickedit() -> None:
|
||||
cmode(True, mode | 4)
|
||||
|
||||
|
||||
def run_argparse(argv: list[str], formatter: Any) -> argparse.Namespace:
|
||||
def run_argparse(argv: list[str], formatter: Any, retry: bool) -> argparse.Namespace:
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=formatter,
|
||||
prog="copyparty",
|
||||
@@ -474,14 +474,14 @@ def run_argparse(argv: list[str], formatter: Any) -> argparse.Namespace:
|
||||
|
||||
ap2 = ap.add_argument_group('upload options')
|
||||
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless -ed")
|
||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
||||
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled")
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600")
|
||||
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload")
|
||||
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even if copyparty thinks you're better off without -- probably useful on nfs and cow filesystems (zfs, btrfs)")
|
||||
ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (within same filesystem)")
|
||||
ap2.add_argument("--never-symlink", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made")
|
||||
ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead")
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600")
|
||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
||||
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; 0 = off and warn if enabled, 1 = off, 2 = on, 3 = on and disable datecheck")
|
||||
|
||||
ap2 = ap.add_argument_group('network options')
|
||||
@@ -611,6 +611,7 @@ def run_argparse(argv: list[str], formatter: Any) -> argparse.Namespace:
|
||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language")
|
||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
|
||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
|
||||
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="favicon text [ foreground [ background ] ], set blank to disable")
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
||||
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
|
||||
@@ -682,12 +683,14 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
except:
|
||||
pass
|
||||
|
||||
for fmtr in [RiceFormatter, Dodge11874, BasicDodge11874]:
|
||||
retry = False
|
||||
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
|
||||
try:
|
||||
al = run_argparse(argv, fmtr)
|
||||
al = run_argparse(argv, fmtr, retry)
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
retry = True
|
||||
lprint("\n[ {} ]:\n{}\n".format(fmtr, min_ex()))
|
||||
|
||||
assert al
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 3, 1)
|
||||
VERSION = (1, 3, 5)
|
||||
CODENAME = "god dag"
|
||||
BUILD_DT = (2022, 6, 16)
|
||||
BUILD_DT = (2022, 7, 6)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
|
||||
|
||||
import ctypes
|
||||
import re
|
||||
import os
|
||||
import time
|
||||
|
||||
from .__init__ import ANYWIN, MACOS
|
||||
@@ -10,7 +11,7 @@ from .authsrv import AXS, VFS
|
||||
from .util import chkcmd, min_ex
|
||||
|
||||
try:
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .util import RootLogger
|
||||
except:
|
||||
@@ -21,25 +22,7 @@ class Fstab(object):
|
||||
def __init__(self, log: RootLogger):
|
||||
self.log_func = log
|
||||
|
||||
self.no_sparse = set(
|
||||
[
|
||||
"fuse", # termux-sdcard
|
||||
"vfat", # linux-efi
|
||||
"fat32",
|
||||
"fat16",
|
||||
"fat12",
|
||||
"fat-32",
|
||||
"fat-16",
|
||||
"fat-12",
|
||||
"fat 32",
|
||||
"fat 16",
|
||||
"fat 12",
|
||||
"exfat",
|
||||
"ex-fat",
|
||||
"ex fat",
|
||||
"hpfs", # macos
|
||||
]
|
||||
)
|
||||
self.trusted = False
|
||||
self.tab: Optional[VFS] = None
|
||||
self.cache: dict[str, str] = {}
|
||||
self.age = 0.0
|
||||
@@ -47,8 +30,8 @@ class Fstab(object):
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func("fstab", msg + "\033[K", c)
|
||||
|
||||
def get(self, path: str):
|
||||
if time.time() - self.age > 600 or len(self.cache) > 9000:
|
||||
def get(self, path: str) -> str:
|
||||
if len(self.cache) > 9000:
|
||||
self.age = time.time()
|
||||
self.tab = None
|
||||
self.cache = {}
|
||||
@@ -68,6 +51,7 @@ class Fstab(object):
|
||||
self.log(msg.format(path, fs, min_ex()), 3)
|
||||
return fs
|
||||
|
||||
path = path.lstrip("/")
|
||||
try:
|
||||
return self.cache[path]
|
||||
except:
|
||||
@@ -83,7 +67,7 @@ class Fstab(object):
|
||||
self.log("found {} at {}".format(fs, path))
|
||||
return fs
|
||||
|
||||
def build_tab(self):
|
||||
def build_tab(self) -> None:
|
||||
self.log("building tab")
|
||||
|
||||
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
||||
@@ -98,7 +82,8 @@ class Fstab(object):
|
||||
if not m:
|
||||
continue
|
||||
|
||||
tab1.append(m.groups())
|
||||
zs1, zs2 = m.groups()
|
||||
tab1.append((str(zs1), str(zs2)))
|
||||
|
||||
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
||||
path1, fs1 = tab1[0]
|
||||
@@ -108,18 +93,53 @@ class Fstab(object):
|
||||
|
||||
self.tab = tab
|
||||
|
||||
def get_unix(self, path: str):
|
||||
def relabel(self, path: str, nval: str) -> None:
|
||||
assert self.tab
|
||||
self.cache = {}
|
||||
path = path.lstrip("/")
|
||||
ptn = re.compile(r"^[^\\/]*")
|
||||
vn, rem = self.tab._find(path)
|
||||
if not self.trusted:
|
||||
# no mtab access; have to build as we go
|
||||
if "/" in rem:
|
||||
self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
|
||||
if rem:
|
||||
self.tab.add(nval, path)
|
||||
else:
|
||||
vn.realpath = nval
|
||||
|
||||
return
|
||||
|
||||
visit = [vn]
|
||||
while visit:
|
||||
vn = visit.pop()
|
||||
vn.realpath = ptn.sub(nval, vn.realpath)
|
||||
visit.extend(list(vn.nodes.values()))
|
||||
|
||||
def get_unix(self, path: str) -> str:
|
||||
if not self.tab:
|
||||
self.build_tab()
|
||||
try:
|
||||
self.build_tab()
|
||||
self.trusted = True
|
||||
except:
|
||||
# prisonparty or other restrictive environment
|
||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||
self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
|
||||
self.trusted = False
|
||||
|
||||
return self.tab._find(path)[0].realpath.split("/")[0]
|
||||
assert self.tab
|
||||
ret = self.tab._find(path)[0]
|
||||
if self.trusted or path == ret.vpath:
|
||||
return ret.realpath.split("/")[0]
|
||||
else:
|
||||
return "idk"
|
||||
|
||||
def get_w32(self, path: str):
|
||||
def get_w32(self, path: str) -> str:
|
||||
# list mountpoints: fsutil fsinfo drives
|
||||
|
||||
from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPDWORD, LPWSTR, MAX_PATH
|
||||
|
||||
def echk(rc, fun, args):
|
||||
def echk(rc: int, fun: Any, args: Any) -> None:
|
||||
if not rc:
|
||||
raise ctypes.WinError(ctypes.get_last_error())
|
||||
return None
|
||||
|
||||
@@ -184,6 +184,8 @@ class HttpCli(object):
|
||||
def j2s(self, name: str, **ka: Any) -> str:
|
||||
tpl = self.conn.hsrv.j2[name]
|
||||
ka["ts"] = self.conn.hsrv.cachebuster()
|
||||
ka["lang"] = self.args.lang
|
||||
ka["favico"] = self.args.favico
|
||||
ka["svcname"] = self.args.doctitle
|
||||
ka["html_head"] = self.html_head
|
||||
return tpl.render(**ka) # type: ignore
|
||||
@@ -545,6 +547,10 @@ class HttpCli(object):
|
||||
static_path = os.path.join(E.mod, "web/", self.vpath[5:])
|
||||
return self.tx_file(static_path)
|
||||
|
||||
if "cf_challenge" in self.uparam:
|
||||
self.reply(self.j2s("cf").encode("utf-8", "replace"))
|
||||
return True
|
||||
|
||||
if not self.can_read and not self.can_write and not self.can_get:
|
||||
if self.vpath:
|
||||
self.log("inaccessible: [{}]".format(self.vpath))
|
||||
@@ -911,6 +917,9 @@ class HttpCli(object):
|
||||
except:
|
||||
raise Pebkac(422, "you POSTed invalid json")
|
||||
|
||||
# self.reply(b" DD" + b"oS Protection ", 503)
|
||||
# return True
|
||||
|
||||
if "srch" in self.uparam or "srch" in body:
|
||||
return self.handle_search(body)
|
||||
|
||||
@@ -1048,7 +1057,7 @@ class HttpCli(object):
|
||||
reader = read_socket(self.sr, remains)
|
||||
|
||||
f = None
|
||||
fpool = not self.args.no_fpool and (not ANYWIN or sprs)
|
||||
fpool = not self.args.no_fpool and sprs
|
||||
if fpool:
|
||||
with self.mutex:
|
||||
try:
|
||||
@@ -1855,7 +1864,9 @@ class HttpCli(object):
|
||||
"edit": "edit" in self.uparam,
|
||||
"title": html_escape(self.vpath, crlf=True),
|
||||
"lastmod": int(ts_md * 1000),
|
||||
"md_plug": "true" if self.args.emp else "false",
|
||||
"lang": self.args.lang,
|
||||
"favico": self.args.favico,
|
||||
"have_emp": self.args.emp,
|
||||
"md_chk_rate": self.args.mcr,
|
||||
"md": boundary,
|
||||
"arg_base": arg_base,
|
||||
@@ -2218,7 +2229,8 @@ class HttpCli(object):
|
||||
ret = json.dumps(ls)
|
||||
mime = "application/json"
|
||||
|
||||
self.reply(ret.encode("utf-8", "replace") + b"\n", mime=mime)
|
||||
ret += "\n\033[0m" if arg == "v" else "\n"
|
||||
self.reply(ret.encode("utf-8", "replace"), mime=mime)
|
||||
return True
|
||||
|
||||
def tx_browser(self) -> bool:
|
||||
@@ -2384,6 +2396,7 @@ class HttpCli(object):
|
||||
"perms": json.dumps(perms),
|
||||
"taglist": [],
|
||||
"def_hcols": [],
|
||||
"have_emp": self.args.emp,
|
||||
"have_up2k_idx": ("e2d" in vn.flags),
|
||||
"have_tags_idx": ("e2t" in vn.flags),
|
||||
"have_acode": (not self.args.no_acode),
|
||||
@@ -2397,11 +2410,20 @@ class HttpCli(object):
|
||||
"readme": readme,
|
||||
"title": html_escape(self.vpath, crlf=True),
|
||||
"srv_info": srv_infot,
|
||||
"lang": self.args.lang,
|
||||
"dtheme": self.args.theme,
|
||||
"themes": self.args.themes,
|
||||
"turbolvl": self.args.turbo,
|
||||
}
|
||||
|
||||
if self.args.js_browser:
|
||||
j2a["js"] = self.args.js_browser
|
||||
|
||||
if self.args.css_browser:
|
||||
j2a["css"] = self.args.css_browser
|
||||
|
||||
if not self.conn.hsrv.prism:
|
||||
j2a["no_prism"] = True
|
||||
|
||||
if not self.can_read:
|
||||
if is_ls:
|
||||
return self.tx_ls(ls_ret)
|
||||
@@ -2601,9 +2623,6 @@ class HttpCli(object):
|
||||
if doctxt is not None:
|
||||
j2a["doc"] = doctxt
|
||||
|
||||
if not self.conn.hsrv.prism:
|
||||
j2a["no_prism"] = True
|
||||
|
||||
for d in dirs:
|
||||
d["name"] += "/"
|
||||
|
||||
@@ -2615,19 +2634,12 @@ class HttpCli(object):
|
||||
else:
|
||||
j2a["files"] = dirs + files
|
||||
|
||||
j2a["logues"] = logues
|
||||
j2a["taglist"] = taglist
|
||||
j2a["txt_ext"] = self.args.textfiles.replace(",", " ")
|
||||
|
||||
if "mth" in vn.flags:
|
||||
j2a["def_hcols"] = vn.flags["mth"].split(",")
|
||||
|
||||
if self.args.js_browser:
|
||||
j2a["js"] = self.args.js_browser
|
||||
|
||||
if self.args.css_browser:
|
||||
j2a["css"] = self.args.css_browser
|
||||
|
||||
html = self.j2s(tpl, **j2a)
|
||||
self.reply(html.encode("utf-8", "replace"))
|
||||
return True
|
||||
|
||||
@@ -81,7 +81,7 @@ class HttpSrv(object):
|
||||
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))
|
||||
self.j2 = {
|
||||
x: env.get_template(x + ".html")
|
||||
for x in ["splash", "browser", "browser2", "msg", "md", "mde"]
|
||||
for x in ["splash", "browser", "browser2", "msg", "md", "mde", "cf"]
|
||||
}
|
||||
self.prism = os.path.exists(os.path.join(E.mod, "web", "deps", "prism.js.gz"))
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
|
||||
from .__init__ import PY2, WINDOWS, unicode
|
||||
from .bos import bos
|
||||
from .util import REKOBO_LKEY, fsenc, retchk, runcmd, uncyg
|
||||
from .util import REKOBO_LKEY, fsenc, min_ex, retchk, runcmd, uncyg
|
||||
|
||||
try:
|
||||
from typing import Any, Union
|
||||
@@ -44,6 +44,7 @@ class MParser(object):
|
||||
|
||||
self.timeout = 30
|
||||
self.force = False
|
||||
self.kill = "t" # tree; all children recursively
|
||||
self.audio = "y"
|
||||
self.ext = []
|
||||
|
||||
@@ -66,6 +67,10 @@ class MParser(object):
|
||||
self.audio = arg[1:] # [r]equire [n]ot [d]ontcare
|
||||
continue
|
||||
|
||||
if arg.startswith("k"):
|
||||
self.kill = arg[1:] # [t]ree [m]ain [n]one
|
||||
continue
|
||||
|
||||
if arg == "f":
|
||||
self.force = True
|
||||
continue
|
||||
@@ -213,7 +218,10 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
||||
fps = ret[".fps"]
|
||||
if "/" in fps:
|
||||
fa, fb = fps.split("/")
|
||||
fps = int(fa) * 1.0 / int(fb)
|
||||
try:
|
||||
fps = int(fa) * 1.0 / int(fb)
|
||||
except:
|
||||
fps = 9001
|
||||
|
||||
if fps < 1000 and fmt.get("format_name") not in ["image2", "png_pipe"]:
|
||||
ret[".fps"] = round(fps, 3)
|
||||
@@ -496,7 +504,7 @@ class MTag(object):
|
||||
if parser.bin.endswith(".py"):
|
||||
cmd = [sys.executable] + cmd
|
||||
|
||||
args = {"env": env, "timeout": parser.timeout}
|
||||
args = {"env": env, "timeout": parser.timeout, "kill": parser.kill}
|
||||
|
||||
if WINDOWS:
|
||||
args["creationflags"] = 0x4000
|
||||
@@ -518,6 +526,8 @@ class MTag(object):
|
||||
if tag and tag in zj:
|
||||
ret[tag] = zj[tag]
|
||||
except:
|
||||
pass
|
||||
if self.args.mtag_v:
|
||||
t = "mtag error: tagname {}, parser {}, file {} => {}"
|
||||
self.log(t.format(tagname, parser.bin, abspath, min_ex()))
|
||||
|
||||
return ret
|
||||
|
||||
@@ -64,8 +64,8 @@ class SvcHub(object):
|
||||
|
||||
if args.ss or args.s >= 2:
|
||||
args.s = True
|
||||
args.no_dot_mv = True
|
||||
args.no_dot_ren = True
|
||||
args.no_logues = True
|
||||
args.no_readme = True
|
||||
args.unpost = 0
|
||||
args.no_del = True
|
||||
args.no_mv = True
|
||||
|
||||
@@ -128,5 +128,8 @@ class ThumbCli(object):
|
||||
if abort:
|
||||
return None
|
||||
|
||||
if not bos.path.getsize(os.path.join(ptop, rem)):
|
||||
return None
|
||||
|
||||
x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt)
|
||||
return x.get() # type: ignore
|
||||
|
||||
@@ -106,6 +106,7 @@ class Up2k(object):
|
||||
self.pending_tags: list[tuple[set[str], str, str, dict[str, Any]]] = []
|
||||
self.hashq: Queue[tuple[str, str, str, str, float]] = Queue()
|
||||
self.tagq: Queue[tuple[str, str, str, str]] = Queue()
|
||||
self.tag_event = threading.Condition()
|
||||
self.n_hashq = 0
|
||||
self.n_tagq = 0
|
||||
self.mpool_used = False
|
||||
@@ -958,6 +959,7 @@ class Up2k(object):
|
||||
to_delete = {}
|
||||
in_progress = {}
|
||||
while True:
|
||||
did_nothing = True
|
||||
with self.mutex:
|
||||
if gid != self.gid:
|
||||
break
|
||||
@@ -967,6 +969,9 @@ class Up2k(object):
|
||||
warks = [str(x[0]) for x in zq]
|
||||
jobs = []
|
||||
for w in warks:
|
||||
if w in in_progress:
|
||||
continue
|
||||
|
||||
q = "select rd, fn from up where substr(w,1,16)=? limit 1"
|
||||
rd, fn = cur.execute(q, (w,)).fetchone()
|
||||
rd, fn = s3dec(rd, fn)
|
||||
@@ -976,15 +981,13 @@ class Up2k(object):
|
||||
zq2 = cur.execute(q, (w,)).fetchall()
|
||||
have: dict[str, Union[str, float]] = {x[0]: 1 for x in zq2}
|
||||
|
||||
did_nothing = False
|
||||
parsers = self._get_parsers(ptop, have, abspath)
|
||||
if not parsers:
|
||||
to_delete[w] = True
|
||||
n_left -= 1
|
||||
continue
|
||||
|
||||
if w in in_progress:
|
||||
continue
|
||||
|
||||
jobs.append(Mpqe(parsers, set(), w, abspath))
|
||||
in_progress[w] = True
|
||||
|
||||
@@ -992,6 +995,7 @@ class Up2k(object):
|
||||
done = self._flush_mpool(wcur)
|
||||
for w in done:
|
||||
to_delete[w] = True
|
||||
did_nothing = False
|
||||
in_progress.pop(w)
|
||||
n_done += 1
|
||||
|
||||
@@ -1004,6 +1008,10 @@ class Up2k(object):
|
||||
if not warks:
|
||||
break
|
||||
|
||||
if did_nothing:
|
||||
with self.tag_event:
|
||||
self.tag_event.wait(0.2)
|
||||
|
||||
if not jobs:
|
||||
continue
|
||||
|
||||
@@ -1132,6 +1140,10 @@ class Up2k(object):
|
||||
except:
|
||||
ex = traceback.format_exc()
|
||||
self._log_tag_err(qe.mtp or self.mtag.backend, qe.abspath, ex)
|
||||
finally:
|
||||
if qe.mtp:
|
||||
with self.tag_event:
|
||||
self.tag_event.notify_all()
|
||||
|
||||
q.task_done()
|
||||
|
||||
@@ -1326,7 +1338,7 @@ class Up2k(object):
|
||||
|
||||
# check if filesystem supports sparse files;
|
||||
# refuse out-of-order / multithreaded uploading if sprs False
|
||||
sprs = self.fstab.get(pdir) not in self.fstab.no_sparse
|
||||
sprs = self.fstab.get(pdir) != "ng"
|
||||
|
||||
with self.mutex:
|
||||
cur = self.cur.get(cj["ptop"])
|
||||
@@ -1359,7 +1371,7 @@ class Up2k(object):
|
||||
"prel": dp_dir,
|
||||
"vtop": cj["vtop"],
|
||||
"ptop": cj["ptop"],
|
||||
"sprs": sprs,
|
||||
"sprs": sprs, # dontcare; finished anyways
|
||||
"size": dsize,
|
||||
"lmod": dtime,
|
||||
"addr": ip,
|
||||
@@ -1494,7 +1506,7 @@ class Up2k(object):
|
||||
"purl": purl,
|
||||
"size": job["size"],
|
||||
"lmod": job["lmod"],
|
||||
"sprs": sprs,
|
||||
"sprs": job.get("sprs", sprs),
|
||||
"hash": job["need"],
|
||||
"wark": wark,
|
||||
}
|
||||
@@ -2215,20 +2227,45 @@ class Up2k(object):
|
||||
suffix = "-{:.6f}-{}".format(job["t0"], dip)
|
||||
with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as zfw:
|
||||
f, job["tnam"] = zfw["orz"]
|
||||
abspath = os.path.join(pdir, job["tnam"])
|
||||
sprs = job["sprs"]
|
||||
sz = job["size"]
|
||||
relabel = False
|
||||
if (
|
||||
ANYWIN
|
||||
and job["sprs"]
|
||||
and sprs
|
||||
and self.args.sparse
|
||||
and self.args.sparse * 1024 * 1024 <= job["size"]
|
||||
and self.args.sparse * 1024 * 1024 <= sz
|
||||
):
|
||||
fp = os.path.join(pdir, job["tnam"])
|
||||
try:
|
||||
sp.check_call(["fsutil", "sparse", "setflag", fp])
|
||||
sp.check_call(["fsutil", "sparse", "setflag", abspath])
|
||||
except:
|
||||
self.log("could not sparse [{}]".format(fp), 3)
|
||||
self.log("could not sparse [{}]".format(abspath), 3)
|
||||
relabel = True
|
||||
sprs = False
|
||||
|
||||
if job["hash"] and job["sprs"]:
|
||||
f.seek(job["size"] - 1)
|
||||
if not ANYWIN and sprs and sz > 1024 * 1024:
|
||||
fs = self.fstab.get(pdir)
|
||||
if fs != "ok":
|
||||
relabel = True
|
||||
f.seek(1024 * 1024 - 1)
|
||||
f.write(b"e")
|
||||
f.flush()
|
||||
try:
|
||||
nblk = bos.stat(abspath).st_blocks
|
||||
sprs = nblk < 2048
|
||||
except:
|
||||
sprs = False
|
||||
|
||||
if relabel:
|
||||
t = "sparse files {} on {} filesystem at {}"
|
||||
nv = "ok" if sprs else "ng"
|
||||
self.log(t.format(nv, self.fstab.get(pdir), pdir))
|
||||
self.fstab.relabel(pdir, nv)
|
||||
job["sprs"] = sprs
|
||||
|
||||
if job["hash"] and sprs:
|
||||
f.seek(sz - 1)
|
||||
f.write(b"e")
|
||||
|
||||
if not job["hash"]:
|
||||
|
||||
@@ -30,6 +30,12 @@ try:
|
||||
except:
|
||||
HAVE_SQLITE3 = False
|
||||
|
||||
try:
|
||||
HAVE_PSUTIL = True
|
||||
import psutil
|
||||
except:
|
||||
HAVE_PSUTIL = False
|
||||
|
||||
try:
|
||||
import types
|
||||
from collections.abc import Callable, Iterable
|
||||
@@ -121,6 +127,7 @@ HTTPCODE = {
|
||||
429: "Too Many Requests",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
503: "Service Unavailable",
|
||||
}
|
||||
|
||||
|
||||
@@ -589,6 +596,7 @@ def ren_open(
|
||||
ext = bname[ofs:] + ext
|
||||
bname = bname[:ofs]
|
||||
|
||||
asciified = False
|
||||
b64 = ""
|
||||
while True:
|
||||
try:
|
||||
@@ -614,11 +622,20 @@ def ren_open(
|
||||
|
||||
except OSError as ex_:
|
||||
ex = ex_
|
||||
|
||||
if ex.errno == 22 and not asciified:
|
||||
asciified = True
|
||||
bname, fname = [
|
||||
zs.encode("ascii", "replace").decode("ascii").replace("?", "_")
|
||||
for zs in [bname, fname]
|
||||
]
|
||||
continue
|
||||
|
||||
if ex.errno not in [36, 63] and (not WINDOWS or ex.errno != 22):
|
||||
raise
|
||||
|
||||
if not b64:
|
||||
zs = (bname + ext).encode("utf-8", "replace")
|
||||
zs = (orig_name + "\n" + suffix).encode("utf-8", "replace")
|
||||
zs = hashlib.sha512(zs).digest()[:12]
|
||||
b64 = base64.urlsafe_b64encode(zs).decode("utf-8")
|
||||
|
||||
@@ -1479,9 +1496,76 @@ def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
|
||||
return ret
|
||||
|
||||
|
||||
def getalive(pids: list[int], pgid: int) -> list[int]:
|
||||
alive = []
|
||||
for pid in pids:
|
||||
try:
|
||||
if pgid:
|
||||
# check if still one of ours
|
||||
if os.getpgid(pid) == pgid:
|
||||
alive.append(pid)
|
||||
else:
|
||||
# windows doesn't have pgroups; assume
|
||||
psutil.Process(pid)
|
||||
alive.append(pid)
|
||||
except:
|
||||
pass
|
||||
|
||||
return alive
|
||||
|
||||
|
||||
def killtree(root: int) -> None:
|
||||
"""still racy but i tried"""
|
||||
try:
|
||||
# limit the damage where possible (unixes)
|
||||
pgid = os.getpgid(os.getpid())
|
||||
except:
|
||||
pgid = 0
|
||||
|
||||
if HAVE_PSUTIL:
|
||||
pids = [root]
|
||||
parent = psutil.Process(root)
|
||||
for child in parent.children(recursive=True):
|
||||
pids.append(child.pid)
|
||||
child.terminate()
|
||||
parent.terminate()
|
||||
parent = None
|
||||
elif pgid:
|
||||
# linux-only
|
||||
pids = []
|
||||
chk = [root]
|
||||
while chk:
|
||||
pid = chk[0]
|
||||
chk = chk[1:]
|
||||
pids.append(pid)
|
||||
_, t, _ = runcmd(["pgrep", "-P", str(pid)])
|
||||
chk += [int(x) for x in t.strip().split("\n") if x]
|
||||
|
||||
pids = getalive(pids, pgid) # filter to our pgroup
|
||||
for pid in pids:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
else:
|
||||
# windows gets minimal effort sorry
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
return
|
||||
|
||||
for n in range(10):
|
||||
time.sleep(0.1)
|
||||
pids = getalive(pids, pgid)
|
||||
if not pids or n > 3 and pids == [root]:
|
||||
break
|
||||
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def runcmd(
|
||||
argv: Union[list[bytes], list[str]], timeout: Optional[int] = None, **ka: Any
|
||||
) -> tuple[int, str, str]:
|
||||
kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one
|
||||
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE, **ka)
|
||||
if not timeout or PY2:
|
||||
stdout, stderr = p.communicate()
|
||||
@@ -1489,12 +1573,27 @@ def runcmd(
|
||||
try:
|
||||
stdout, stderr = p.communicate(timeout=timeout)
|
||||
except sp.TimeoutExpired:
|
||||
p.kill()
|
||||
stdout, stderr = p.communicate()
|
||||
if kill == "n":
|
||||
return -18, "", "" # SIGCONT; leave it be
|
||||
elif kill == "m":
|
||||
p.kill()
|
||||
else:
|
||||
killtree(p.pid)
|
||||
|
||||
try:
|
||||
stdout, stderr = p.communicate(timeout=1)
|
||||
except:
|
||||
stdout = b""
|
||||
stderr = b""
|
||||
|
||||
stdout = stdout.decode("utf-8", "replace")
|
||||
stderr = stderr.decode("utf-8", "replace")
|
||||
return p.returncode, stdout, stderr
|
||||
|
||||
rc = p.returncode
|
||||
if rc is None:
|
||||
rc = -14 # SIGALRM; failed to kill
|
||||
|
||||
return rc, stdout, stderr
|
||||
|
||||
|
||||
def chkcmd(argv: Union[list[bytes], list[str]], **ka: Any) -> tuple[str, str]:
|
||||
@@ -1695,16 +1794,16 @@ def termsize() -> tuple[int, int]:
|
||||
# from hashwalk
|
||||
env = os.environ
|
||||
|
||||
def ioctl_GWINSZ(fd):
|
||||
def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]:
|
||||
try:
|
||||
import fcntl
|
||||
import struct
|
||||
import termios
|
||||
|
||||
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
|
||||
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, b"1234"))
|
||||
return int(cr[1]), int(cr[0])
|
||||
except:
|
||||
return
|
||||
return cr
|
||||
return None
|
||||
|
||||
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
|
||||
if not cr:
|
||||
@@ -1715,13 +1814,13 @@ def termsize() -> tuple[int, int]:
|
||||
except:
|
||||
pass
|
||||
|
||||
if not cr:
|
||||
try:
|
||||
cr = (env["LINES"], env["COLUMNS"])
|
||||
except:
|
||||
cr = (25, 80)
|
||||
if cr:
|
||||
return cr
|
||||
|
||||
return int(cr[1]), int(cr[0])
|
||||
try:
|
||||
return int(env["COLUMNS"]), int(env["LINES"])
|
||||
except:
|
||||
return 80, 25
|
||||
|
||||
|
||||
class Pebkac(Exception):
|
||||
|
||||
@@ -21,7 +21,7 @@ window.baguetteBox = (function () {
|
||||
afterHide: null,
|
||||
onChange: null,
|
||||
},
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnVmode, btnClose,
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnClose,
|
||||
currentGallery = [],
|
||||
currentIndex = 0,
|
||||
isOverlayVisible = false,
|
||||
@@ -37,6 +37,9 @@ window.baguetteBox = (function () {
|
||||
vmute = false,
|
||||
vloop = sread('vmode') == 'L',
|
||||
vnext = sread('vmode') == 'C',
|
||||
loopA = null,
|
||||
loopB = null,
|
||||
url_ts = null,
|
||||
resume_mp = false;
|
||||
|
||||
var onFSC = function (e) {
|
||||
@@ -182,6 +185,7 @@ window.baguetteBox = (function () {
|
||||
'<button id="bbox-rotl" type="button">↶</button>' +
|
||||
'<button id="bbox-rotr" type="button">↷</button>' +
|
||||
'<button id="bbox-tsel" type="button">sel</button>' +
|
||||
'<button id="bbox-full" 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>'
|
||||
@@ -198,9 +202,9 @@ window.baguetteBox = (function () {
|
||||
btnRotL = ebi('bbox-rotl');
|
||||
btnRotR = ebi('bbox-rotr');
|
||||
btnSel = ebi('bbox-tsel');
|
||||
btnFull = ebi('bbox-full');
|
||||
btnVmode = ebi('bbox-vmode');
|
||||
btnClose = ebi('bbox-close');
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
function halp() {
|
||||
@@ -215,6 +219,7 @@ window.baguetteBox = (function () {
|
||||
['home', 'first file'],
|
||||
['end', 'last file'],
|
||||
['R', 'rotate (shift=ccw)'],
|
||||
['F', 'toggle fullscreen'],
|
||||
['S', 'toggle file selection'],
|
||||
['space, P, K', 'video: play / pause'],
|
||||
['U', 'video: seek 10sec back'],
|
||||
@@ -222,7 +227,7 @@ window.baguetteBox = (function () {
|
||||
['M', 'video: toggle mute'],
|
||||
['V', 'video: toggle loop'],
|
||||
['C', 'video: toggle auto-next'],
|
||||
['F', 'video: toggle fullscreen'],
|
||||
['<code>[</code>, <code>]</code>', 'video: loop start / end'],
|
||||
],
|
||||
d = mknod('table'),
|
||||
html = ['<tbody>'];
|
||||
@@ -230,6 +235,8 @@ window.baguetteBox = (function () {
|
||||
for (var a = 0; a < list.length; a++)
|
||||
html.push('<tr><td>' + list[a][0] + '</td><td>' + list[a][1] + '</td></tr>');
|
||||
|
||||
html.push('<tr><td colspan="2">tap middle of img to hide btns</td></tr>');
|
||||
html.push('<tr><td colspan="2">tap left/right sides for prev/next</td></tr>');
|
||||
d.innerHTML = html.join('\n') + '</tbody>';
|
||||
d.setAttribute('id', 'bbox-halp');
|
||||
d.onclick = function () {
|
||||
@@ -273,19 +280,17 @@ window.baguetteBox = (function () {
|
||||
setVmode();
|
||||
}
|
||||
else if (k == "KeyF")
|
||||
try {
|
||||
if (isFullscreen)
|
||||
document.exitFullscreen();
|
||||
else
|
||||
v.requestFullscreen();
|
||||
}
|
||||
catch (ex) { }
|
||||
tglfull();
|
||||
else if (k == "KeyS")
|
||||
tglsel();
|
||||
else if (k == "KeyR")
|
||||
rotn(e.shiftKey ? -1 : 1);
|
||||
else if (k == "KeyY")
|
||||
dlpic();
|
||||
else if (k == "BracketLeft")
|
||||
setloop(1);
|
||||
else if (k == "BracketRight")
|
||||
setloop(2);
|
||||
}
|
||||
|
||||
function anim() {
|
||||
@@ -354,6 +359,16 @@ window.baguetteBox = (function () {
|
||||
return [name, a, files, ebi(files[a].id)];
|
||||
}
|
||||
|
||||
function tglfull() {
|
||||
try {
|
||||
if (isFullscreen)
|
||||
document.exitFullscreen();
|
||||
else
|
||||
(vid() || ebi('bbox-overlay')).requestFullscreen();
|
||||
}
|
||||
catch (ex) { alert(ex); }
|
||||
}
|
||||
|
||||
function tglsel() {
|
||||
var o = findfile()[3];
|
||||
clmod(o.closest('tr'), 'sel', 't');
|
||||
@@ -416,6 +431,9 @@ window.baguetteBox = (function () {
|
||||
var nonPassiveEvent = passiveSupp ? { passive: true } : null;
|
||||
|
||||
function bindEvents() {
|
||||
bind(document, 'keydown', keyDownHandler);
|
||||
bind(document, 'keyup', keyUpHandler);
|
||||
bind(document, 'fullscreenchange', onFSC);
|
||||
bind(overlay, 'click', overlayClickHandler);
|
||||
bind(btnPrev, 'click', showPreviousImage);
|
||||
bind(btnNext, 'click', showNextImage);
|
||||
@@ -426,6 +444,7 @@ window.baguetteBox = (function () {
|
||||
bind(btnRotL, 'click', rotl);
|
||||
bind(btnRotR, 'click', rotr);
|
||||
bind(btnSel, 'click', tglsel);
|
||||
bind(btnFull, 'click', tglfull);
|
||||
bind(slider, 'contextmenu', contextmenuHandler);
|
||||
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||
@@ -434,6 +453,9 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function unbindEvents() {
|
||||
unbind(document, 'keydown', keyDownHandler);
|
||||
unbind(document, 'keyup', keyUpHandler);
|
||||
unbind(document, 'fullscreenchange', onFSC);
|
||||
unbind(overlay, 'click', overlayClickHandler);
|
||||
unbind(btnPrev, 'click', showPreviousImage);
|
||||
unbind(btnNext, 'click', showNextImage);
|
||||
@@ -444,6 +466,7 @@ window.baguetteBox = (function () {
|
||||
unbind(btnRotL, 'click', rotl);
|
||||
unbind(btnRotR, 'click', rotr);
|
||||
unbind(btnSel, 'click', tglsel);
|
||||
unbind(btnFull, 'click', tglfull);
|
||||
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||
@@ -508,9 +531,7 @@ window.baguetteBox = (function () {
|
||||
if (overlay.style.display === 'block')
|
||||
return;
|
||||
|
||||
bind(document, 'keydown', keyDownHandler);
|
||||
bind(document, 'keyup', keyUpHandler);
|
||||
bind(document, 'fullscreenchange', onFSC);
|
||||
bindEvents();
|
||||
currentIndex = chosenImageIndex;
|
||||
touch = {
|
||||
count: 0,
|
||||
@@ -522,6 +543,10 @@ window.baguetteBox = (function () {
|
||||
preloadPrev(currentIndex);
|
||||
});
|
||||
|
||||
clmod(ebi('bbox-btns'), 'off');
|
||||
clmod(btnPrev, 'off');
|
||||
clmod(btnNext, 'off');
|
||||
|
||||
updateOffset();
|
||||
overlay.style.display = 'block';
|
||||
// Fade in overlay
|
||||
@@ -534,9 +559,10 @@ window.baguetteBox = (function () {
|
||||
options.afterShow();
|
||||
}, 50);
|
||||
|
||||
if (options.onChange)
|
||||
if (options.onChange && !url_ts)
|
||||
options.onChange(currentIndex, imagesElements.length);
|
||||
|
||||
url_ts = null;
|
||||
documentLastFocus = document.activeElement;
|
||||
btnClose.focus();
|
||||
isOverlayVisible = true;
|
||||
@@ -553,9 +579,13 @@ window.baguetteBox = (function () {
|
||||
return;
|
||||
|
||||
sethash('');
|
||||
unbind(document, 'keydown', keyDownHandler);
|
||||
unbind(document, 'keyup', keyUpHandler);
|
||||
unbind(document, 'fullscreenchange', onFSC);
|
||||
unbindEvents();
|
||||
try {
|
||||
document.exitFullscreen();
|
||||
isFullscreen = false;
|
||||
}
|
||||
catch (ex) { }
|
||||
|
||||
// Fade out and hide the overlay
|
||||
overlay.className = '';
|
||||
setTimeout(function () {
|
||||
@@ -789,8 +819,18 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function playvid(play) {
|
||||
if (vid())
|
||||
vid()[play ? 'play' : 'pause']();
|
||||
if (!play) {
|
||||
timer.rm(loopchk);
|
||||
loopA = loopB = null;
|
||||
}
|
||||
|
||||
var v = vid();
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
v[play ? 'play' : 'pause']();
|
||||
if (play && loopA !== null && v.currentTime < loopA)
|
||||
v.currentTime = loopA;
|
||||
}
|
||||
|
||||
function playpause() {
|
||||
@@ -809,6 +849,38 @@ window.baguetteBox = (function () {
|
||||
showNextImage();
|
||||
}
|
||||
|
||||
function setloop(side) {
|
||||
var v = vid();
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
var t = v.currentTime;
|
||||
if (side == 1) loopA = t;
|
||||
if (side == 2) loopB = t;
|
||||
if (side)
|
||||
toast.inf(5, 'Loop' + (side == 1 ? 'A' : 'B') + ': ' + f2f(t, 2));
|
||||
|
||||
if (loopB !== null) {
|
||||
timer.add(loopchk);
|
||||
sethash(window.location.hash.slice(1).split('&')[0] + '&t=' + (loopA || 0) + '-' + loopB);
|
||||
}
|
||||
}
|
||||
|
||||
function loopchk() {
|
||||
if (loopB === null)
|
||||
return;
|
||||
|
||||
var v = vid();
|
||||
if (!v || v.paused || v.currentTime < loopB)
|
||||
return;
|
||||
|
||||
v.currentTime = loopA || 0;
|
||||
}
|
||||
|
||||
function urltime(txt) {
|
||||
url_ts = txt;
|
||||
}
|
||||
|
||||
function mp_ctl() {
|
||||
var v = vid();
|
||||
if (!vmute && v && mp.au && !mp.au.paused) {
|
||||
@@ -851,6 +923,15 @@ window.baguetteBox = (function () {
|
||||
playvid(true);
|
||||
v.muted = vmute;
|
||||
v.loop = vloop;
|
||||
if (url_ts) {
|
||||
var seek = ('' + url_ts).split('-');
|
||||
v.currentTime = seek[0];
|
||||
if (seek.length > 1) {
|
||||
loopA = parseFloat(seek[0]);
|
||||
loopB = parseFloat(seek[1]);
|
||||
setloop();
|
||||
}
|
||||
}
|
||||
}
|
||||
selbg();
|
||||
mp_ctl();
|
||||
@@ -862,6 +943,22 @@ window.baguetteBox = (function () {
|
||||
else
|
||||
timer.rm(rotn);
|
||||
|
||||
el.onclick = function (e) {
|
||||
var rc = e.target.getBoundingClientRect(),
|
||||
x = e.clientX - rc.left,
|
||||
fx = x / (rc.right - rc.left);
|
||||
|
||||
if (fx < 0.3)
|
||||
return showPreviousImage();
|
||||
|
||||
if (fx > 0.7)
|
||||
return showNextImage();
|
||||
|
||||
clmod(ebi('bbox-btns'), 'off', 't');
|
||||
clmod(btnPrev, 'off', 't');
|
||||
clmod(btnNext, 'off', 't');
|
||||
};
|
||||
|
||||
var prev = QS('.full-image.vis');
|
||||
if (prev)
|
||||
clmod(prev, 'vis');
|
||||
@@ -898,8 +995,6 @@ window.baguetteBox = (function () {
|
||||
function destroyPlugin() {
|
||||
unbindEvents();
|
||||
clearCachedData();
|
||||
unbind(document, 'keydown', keyDownHandler);
|
||||
unbind(document, 'keyup', keyUpHandler);
|
||||
document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));
|
||||
data = {};
|
||||
currentGallery = [];
|
||||
@@ -912,6 +1007,7 @@ window.baguetteBox = (function () {
|
||||
showNext: showNextImage,
|
||||
showPrevious: showPreviousImage,
|
||||
relseek: relseek,
|
||||
urltime: urltime,
|
||||
playpause: playpause,
|
||||
hide: hideOverlay,
|
||||
destroy: destroyPlugin
|
||||
|
||||
@@ -238,6 +238,7 @@ html.b {
|
||||
--u2-txt-bg: transparent;
|
||||
--u2-tab-1-sh: var(--bg);
|
||||
--u2-b1-bg: rgba(128,128,128,0.15);
|
||||
--u2-b2-bg: var(--u2-b1-bg);
|
||||
|
||||
--u2-o-bg: var(--btn-bg);
|
||||
--u2-o-h-bg: var(--btn-h-bg);
|
||||
@@ -352,6 +353,8 @@ html.cy {
|
||||
--srv-1: #f00;
|
||||
--op-aa-bg: #fff;
|
||||
|
||||
--u2-b1-bg: #f00;
|
||||
--u2-b2-bg: #f00;
|
||||
--u2-o-bg: #ff0;
|
||||
--u2-o-1-bg: #f00;
|
||||
}
|
||||
@@ -371,7 +374,7 @@ html.dz {
|
||||
--bg: #010;
|
||||
--bgg: var(--bg);
|
||||
--bg-d1: #000;
|
||||
--bg-d2: #000;
|
||||
--bg-d2: #020;
|
||||
--bg-d3: #000;
|
||||
--bg-max: #000;
|
||||
|
||||
@@ -381,8 +384,8 @@ html.dz {
|
||||
--scroll: #0f0;
|
||||
|
||||
--a: #9f9;
|
||||
--a-b: #fff;
|
||||
--a-hil: #fff;
|
||||
--a-b: #cfc;
|
||||
--a-hil: #cfc;
|
||||
--a-dark: #afa;
|
||||
--a-gray: #2a2;
|
||||
|
||||
@@ -412,8 +415,8 @@ html.dz {
|
||||
--u2-tab-1-b2: #583;
|
||||
--u2-tab-1-sh: #280;
|
||||
--u2-b-fg: #fff;
|
||||
--u2-b1-bg: #c38;
|
||||
--u2-b2-bg: #d80;
|
||||
--u2-b1-bg: #3a3;
|
||||
--u2-b2-bg: #3a3;
|
||||
--u2-inf-bg: #07a;
|
||||
--u2-inf-b1: #0be;
|
||||
--u2-ok-bg: #380;
|
||||
@@ -458,7 +461,7 @@ html.dz {
|
||||
--f-sh1: 0.33;
|
||||
--f-sh2: 0.02;
|
||||
--f-sh3: 0.2;
|
||||
--f-h-b1: rgba(128,128,128,0.7);
|
||||
--f-h-b1: #3b3;
|
||||
|
||||
--f-play-bg: #fc5;
|
||||
--f-play-fg: #000;
|
||||
@@ -522,6 +525,8 @@ html.dy {
|
||||
--u2-tab-1-b2: a;
|
||||
--u2-tab-1-fg: a;
|
||||
--u2-tab-1-bg: a;
|
||||
--u2-b1-bg: #000;
|
||||
--u2-b2-bg: #000;
|
||||
--ud-b1: a;
|
||||
|
||||
--sort-1: a;
|
||||
@@ -713,6 +718,7 @@ html.y #files thead th {
|
||||
}
|
||||
#files td:first-child {
|
||||
border-radius: .25em 0 0 .25em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#files td:last-child {
|
||||
border-radius: 0 .25em .25em 0;
|
||||
@@ -1841,6 +1847,7 @@ a.btn,
|
||||
.full-image img,
|
||||
.full-image video {
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
@@ -1921,6 +1928,15 @@ html.y #bbox-overlay figcaption a {
|
||||
.bbox-btn {
|
||||
position: fixed;
|
||||
}
|
||||
.bbox-btn,
|
||||
#bbox-btns {
|
||||
opacity: 1;
|
||||
animation: opacity .2s infinite ease-in-out;
|
||||
}
|
||||
.bbox-btn.off,
|
||||
#bbox-btns.off {
|
||||
opacity: 0;
|
||||
}
|
||||
#bbox-overlay button {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
@@ -1965,7 +1981,7 @@ html.y #bbox-overlay figcaption a {
|
||||
#bbox-halp td {
|
||||
padding: .2em .5em;
|
||||
}
|
||||
#bbox-halp td:first-child {
|
||||
#bbox-halp td:first-child:not([colspan]) {
|
||||
text-align: right;
|
||||
}
|
||||
.bbox-spinner {
|
||||
@@ -2205,6 +2221,9 @@ html.y #bbox-overlay figcaption a {
|
||||
transition: min-height .2s;
|
||||
margin: 2em 0;
|
||||
}
|
||||
#u2tabw.na>table {
|
||||
display: none;
|
||||
}
|
||||
#u2tab {
|
||||
border-collapse: collapse;
|
||||
width: calc(100% - 2em);
|
||||
@@ -2369,6 +2388,9 @@ html.y #bbox-overlay figcaption a {
|
||||
position: relative;
|
||||
bottom: -0.08em;
|
||||
}
|
||||
#u2conf input+a.b {
|
||||
background: var(--u2-b2-bg);
|
||||
}
|
||||
html.b #u2conf a.b:hover {
|
||||
background: var(--btn-h-bg);
|
||||
}
|
||||
@@ -2598,9 +2620,6 @@ html.c #u2cards,
|
||||
html.a #u2cards {
|
||||
margin: 0 auto -1em auto;
|
||||
}
|
||||
html.a #u2conf input+a.b {
|
||||
background: var(--u2-b2-bg);
|
||||
}
|
||||
html.c #u2foot:empty,
|
||||
html.a #u2foot:empty {
|
||||
margin-bottom: -1em;
|
||||
@@ -2778,6 +2797,17 @@ html.cy #files tbody div a:last-child {
|
||||
|
||||
|
||||
|
||||
|
||||
html.dz * {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
html.d #treepar {
|
||||
border-bottom: .2em solid var(--f-h-b1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media (min-width: 70em) {
|
||||
#barpos,
|
||||
#barbuf {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>⇆🎉 {{ title }}</title>
|
||||
<title>{{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
{{ html_head }}
|
||||
@@ -139,6 +139,7 @@
|
||||
dtheme = "{{ dtheme }}",
|
||||
srvinf = "{{ srv_info }}",
|
||||
lang = "{{ lang }}",
|
||||
dfavico = "{{ favico }}",
|
||||
def_hcols = {{ def_hcols|tojson }},
|
||||
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||
@@ -148,6 +149,7 @@
|
||||
have_unpost = {{ have_unpost|tojson }},
|
||||
have_zip = {{ have_zip|tojson }},
|
||||
turbolvl = {{ turbolvl|tojson }},
|
||||
have_emp = {{ have_emp|tojson }},
|
||||
txt_ext = "{{ txt_ext }}",
|
||||
{% if no_prism %}no_prism = 1,{% endif %}
|
||||
readme = {{ readme|tojson }},
|
||||
|
||||
@@ -36,8 +36,8 @@ var Ls = {
|
||||
"ot_msg": "msg: send a message to the server log",
|
||||
"ot_mp": "media player options",
|
||||
"ot_cfg": "configuration options",
|
||||
"ot_u2i": 'up2k: upload files (if you have write-access) or toggle into the search-mode to see if they exist somewhere on the server$N$Nuploads are resumable, multithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader',
|
||||
"ot_u2w": 'up2k: upload files with resume support (close your browser and drop the same files in later)$N$Nmultithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader',
|
||||
"ot_u2i": 'up2k: upload files (if you have write-access) or toggle into the search-mode to see if they exist somewhere on the server$N$Nuploads are resumable, multithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader<br /><br />during uploads, this icon becomes a progress indicator!',
|
||||
"ot_u2w": 'up2k: upload files with resume support (close your browser and drop the same files in later)$N$Nmultithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader<br /><br />during uploads, this icon becomes a progress indicator!',
|
||||
|
||||
"ab_mkdir": "make directory",
|
||||
"ab_mkdoc": "new markdown doc",
|
||||
@@ -248,6 +248,7 @@ var Ls = {
|
||||
"md_eshow": "cannot show ",
|
||||
|
||||
"xhr403": "403: Access denied\n\ntry pressing F5, maybe you got logged out",
|
||||
"cf_ok": "sorry about that -- DD" + wah + "oS protection kicked in\n\nthings should resume in about 30 sec\n\nif nothing happens, hit F5 to reload the page",
|
||||
"tl_xe1": "could not list subfolders:\n\nerror ",
|
||||
"tl_xe2": "404: Folder not found",
|
||||
"fl_xe1": "could not list files in folder:\n\nerror ",
|
||||
@@ -360,8 +361,8 @@ var Ls = {
|
||||
"ot_msg": "msg: send en beskjed til serverloggen",
|
||||
"ot_mp": "musikkspiller-instillinger",
|
||||
"ot_cfg": "andre innstillinger",
|
||||
"ot_u2i": 'up2k: last opp filer (hvis du har skrivetilgang) eller bytt til søkemodus for å sjekke om filene finnes et-eller-annet sted på serveren$N$Nopplastninger kan gjenopptas etter avbrudd, skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren bup',
|
||||
"ot_u2w": 'up2k: filopplastning med støtte for å gjenoppta avbrutte opplastninger -- steng ned nettleseren og dra de samme filene inn i nettleseren igjen for å plukke opp igjen der du slapp$N$Nopplastninger skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren "bup"',
|
||||
"ot_u2i": 'up2k: last opp filer (hvis du har skrivetilgang) eller bytt til søkemodus for å sjekke om filene finnes et-eller-annet sted på serveren$N$Nopplastninger kan gjenopptas etter avbrudd, skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren bup<br /><br />mens opplastninger foregår så vises fremdriften her oppe!',
|
||||
"ot_u2w": 'up2k: filopplastning med støtte for å gjenoppta avbrutte opplastninger -- steng ned nettleseren og dra de samme filene inn i nettleseren igjen for å plukke opp igjen der du slapp$N$Nopplastninger skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren "bup"<br /><br />mens opplastninger foregår så vises fremdriften her oppe!',
|
||||
|
||||
"ab_mkdir": "lag mappe",
|
||||
"ab_mkdoc": "nytt dokument",
|
||||
@@ -572,6 +573,7 @@ var Ls = {
|
||||
"md_eshow": "kan ikke vise ",
|
||||
|
||||
"xhr403": "403: Tilgang nektet\n\nkanskje du ble logget ut? prøv å trykk F5",
|
||||
"cf_ok": "beklager -- liten tilfeldig kontroll, alt OK\n\nting skal fortsette om ca. 30 sekunder\n\nhvis ikkeno skjer, trykk F5 for å laste siden på nytt",
|
||||
"tl_xe1": "kunne ikke hente undermapper:\n\nfeil ",
|
||||
"tl_xe2": "404: Mappen finnes ikke",
|
||||
"fl_xe1": "kunne ikke hente filer i mappen:\n\nfeil ",
|
||||
@@ -597,8 +599,8 @@ var Ls = {
|
||||
"un_max": "viser de første 2000 filene (bruk filteret for å innsnevre)",
|
||||
"un_avail": "{0} filer kan slettes",
|
||||
"un_m2": "sortert etter opplastningstid – nyeste først:",
|
||||
"un_no1": "men nei, her var det jaggu ingenting",
|
||||
"un_no2": "men nei, her var det jaggu ingenting som passer overens med filteret",
|
||||
"un_no1": "men nei, her var det jaggu ikkeno som slettes kan",
|
||||
"un_no2": "men nei, her var det jaggu ingenting som passet overens med filteret",
|
||||
"un_next": "slett de neste {0} filene nedenfor",
|
||||
"un_del": "slett",
|
||||
"un_m3": "henter listen med nylig opplastede filer...",
|
||||
@@ -761,7 +763,7 @@ ebi('op_up2k').innerHTML = (
|
||||
|
||||
'</div>\n' +
|
||||
|
||||
'<div id="u2tabw"><table id="u2tab">\n' +
|
||||
'<div id="u2tabw" class="na"><table id="u2tab">\n' +
|
||||
' <thead>\n' +
|
||||
' <tr>\n' +
|
||||
' <td>' + L.utl_name + '</td>\n' +
|
||||
@@ -1008,7 +1010,7 @@ var mpl = (function () {
|
||||
'<div><h3>' + L.ml_eq + '</h3><div id="audio_eq"></div></div>');
|
||||
|
||||
var r = {
|
||||
"pb_mode": (sread('pb_mode') || 'loop').split('-')[0],
|
||||
"pb_mode": (sread('pb_mode') || 'next').split('-')[0],
|
||||
"os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl,
|
||||
};
|
||||
bcfg_bind(r, 'preload', 'au_preload', true);
|
||||
@@ -2321,9 +2323,13 @@ function scan_hash(v) {
|
||||
ts = null;
|
||||
|
||||
if (m.length > 3) {
|
||||
m = /^&[Tt=0]*([0-9]+[Mm:])?0*([0-9]+)[Ss]?$/.exec(m[3]);
|
||||
if (m) {
|
||||
ts = parseInt(m[1] || 0) * 60 + parseInt(m[2] || 0);
|
||||
var tm = /^&[Tt=0]*([0-9]+[Mm:])?0*([0-9]+)[Ss]?$/.exec(m[3]);
|
||||
if (tm) {
|
||||
ts = parseInt(tm[1] || 0) * 60 + parseInt(tm[2] || 0);
|
||||
}
|
||||
tm = /^&[Tt=0]*([0-9\.]+)-([0-9\.]+)$/.exec(m[3]);
|
||||
if (tm) {
|
||||
ts = '' + tm[1] + '-' + tm[2];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2359,6 +2365,7 @@ function eval_hash() {
|
||||
return;
|
||||
|
||||
clearInterval(t);
|
||||
baguetteBox.urltime(ts);
|
||||
var im = QS('#ggrid a[ref="' + id + '"]');
|
||||
im.click();
|
||||
im.scrollIntoView();
|
||||
@@ -2644,6 +2651,8 @@ var fileman = (function () {
|
||||
if (!md.hasOwnProperty(k))
|
||||
continue;
|
||||
|
||||
md[k] = (md[k] + '').replace(/[\/\\]/g, '-');
|
||||
|
||||
if (k.startsWith('.'))
|
||||
md[k.slice(1)] = md[k];
|
||||
}
|
||||
@@ -5791,13 +5800,34 @@ function show_md(md, name, div, url, depth) {
|
||||
});
|
||||
}
|
||||
|
||||
md_plug = {}
|
||||
md = load_md_plug(md, 'pre');
|
||||
md = load_md_plug(md, 'post');
|
||||
|
||||
var marked_opts = {
|
||||
headerPrefix: 'md-',
|
||||
breaks: true,
|
||||
gfm: true
|
||||
};
|
||||
var ext = md_plug.pre;
|
||||
if (ext)
|
||||
Object.assign(marked_opts, ext[0]);
|
||||
|
||||
try {
|
||||
clmod(div, 'mdo', 1);
|
||||
div.innerHTML = marked.parse(md, {
|
||||
headerPrefix: 'md-',
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
div.innerHTML = marked.parse(md, marked_opts);
|
||||
|
||||
ext = md_plug.post;
|
||||
ext = ext ? [ext[0].render, ext[0].render2] : [];
|
||||
for (var a = 0; a < ext.length; a++)
|
||||
if (ext[a])
|
||||
try {
|
||||
ext[a](div);
|
||||
}
|
||||
catch (ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
|
||||
var els = QSA('#epi a');
|
||||
for (var a = 0, aa = els.length; a < aa; a++) {
|
||||
var href = els[a].getAttribute('href');
|
||||
@@ -5887,7 +5917,7 @@ var unpost = (function () {
|
||||
html.push("<table><thead><tr><td></td><td>time</td><td>size</td><td>file</td></tr></thead><tbody>");
|
||||
}
|
||||
else
|
||||
html.push(filt.value ? L.un_no2 : L.un_no1);
|
||||
html.push('-- <em>' + (filt.value ? L.un_no2 : L.un_no1) + '</em>');
|
||||
|
||||
var mods = [1000, 100, 10];
|
||||
for (var a = 0; a < res.length; a++) {
|
||||
@@ -6011,6 +6041,9 @@ function wintitle(txt) {
|
||||
|
||||
|
||||
ebi('path').onclick = function (e) {
|
||||
if (ctrl(e))
|
||||
return true;
|
||||
|
||||
var a = e.target.closest('a[href]');
|
||||
if (!a || !(a = a.getAttribute('href') + '') || !a.endsWith('/'))
|
||||
return;
|
||||
@@ -6038,9 +6071,11 @@ ebi('files').onclick = ebi('docul').onclick = function (e) {
|
||||
|
||||
tgt = e.target.closest('a[hl]');
|
||||
if (tgt) {
|
||||
var fun = function () {
|
||||
showfile.show(noq_href(ebi(tgt.getAttribute('hl'))), tgt.getAttribute('lang'));
|
||||
}, szs = ft2dict(tgt.closest('tr'))[0].sz,
|
||||
var a = ebi(tgt.getAttribute('hl')),
|
||||
fun = function () {
|
||||
showfile.show(noq_href(a), tgt.getAttribute('lang'));
|
||||
},
|
||||
szs = ft2dict(a.closest('tr'))[0].sz,
|
||||
sz = parseInt(szs.replace(/[, ]/g, ''));
|
||||
|
||||
if (sz < 1024 * 1024)
|
||||
|
||||
27
copyparty/web/cf.html
Normal file
27
copyparty/web/cf.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ svcname }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="box" style="opacity: 0; font-family: sans-serif">
|
||||
<h3>please press F5 to reload the page</h3>
|
||||
<p>sorry for the inconvenience</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
document.getElementById('box').style.opacity = 1;
|
||||
}, 500);
|
||||
|
||||
parent.toast.ok(30, parent.L.cf_ok);
|
||||
parent.qsr('#cf_frame');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html><html><head>
|
||||
<meta charset="utf-8">
|
||||
<title>📝🎉 {{ title }}</title>
|
||||
<title>📝 {{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
{{ html_head }}
|
||||
@@ -127,10 +127,12 @@ write markdown (most html is 🙆 too)
|
||||
|
||||
<script>
|
||||
|
||||
var last_modified = {{ lastmod }};
|
||||
var last_modified = {{ lastmod }},
|
||||
have_emp = {{ have_emp|tojson }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
link_md_as_html: false,
|
||||
allow_plugins: {{ md_plug }},
|
||||
modpoll_freq: {{ md_chk_rate }}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ var dbg = function () { };
|
||||
// dbg = console.log
|
||||
|
||||
|
||||
// plugins
|
||||
var md_plug = {};
|
||||
|
||||
|
||||
// dodge browser issues
|
||||
(function () {
|
||||
var ua = navigator.userAgent;
|
||||
@@ -160,7 +156,7 @@ function copydom(src, dst, lv) {
|
||||
}
|
||||
|
||||
|
||||
function md_plug_err(ex, js) {
|
||||
md_plug_err = function (ex, js) {
|
||||
qsr('#md_errbox');
|
||||
if (!ex)
|
||||
return;
|
||||
@@ -197,50 +193,12 @@ function md_plug_err(ex, js) {
|
||||
}
|
||||
|
||||
|
||||
function load_plug(md_text, plug_type) {
|
||||
if (!md_opt.allow_plugins)
|
||||
return md_text;
|
||||
|
||||
var find = '\n```copyparty_' + plug_type + '\n';
|
||||
var ofs = md_text.indexOf(find);
|
||||
if (ofs === -1)
|
||||
return md_text;
|
||||
|
||||
var ofs2 = md_text.indexOf('\n```', ofs + 1);
|
||||
if (ofs2 == -1)
|
||||
return md_text;
|
||||
|
||||
var js = md_text.slice(ofs + find.length, ofs2 + 1);
|
||||
var md = md_text.slice(0, ofs + 1) + md_text.slice(ofs2 + 4);
|
||||
|
||||
var old_plug = md_plug[plug_type];
|
||||
if (!old_plug || old_plug[1] != js) {
|
||||
js = 'const x = { ' + js + ' }; x;';
|
||||
try {
|
||||
var x = eval(js);
|
||||
}
|
||||
catch (ex) {
|
||||
md_plug[plug_type] = null;
|
||||
md_plug_err(ex, js);
|
||||
return md;
|
||||
}
|
||||
if (x['ctor']) {
|
||||
x['ctor']();
|
||||
delete x['ctor'];
|
||||
}
|
||||
md_plug[plug_type] = [x, js];
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
|
||||
function convert_markdown(md_text, dest_dom) {
|
||||
md_text = md_text.replace(/\r/g, '');
|
||||
|
||||
md_plug_err(null);
|
||||
md_text = load_plug(md_text, 'pre');
|
||||
md_text = load_plug(md_text, 'post');
|
||||
md_text = load_md_plug(md_text, 'pre');
|
||||
md_text = load_md_plug(md_text, 'post');
|
||||
|
||||
var marked_opts = {
|
||||
//headerPrefix: 'h-',
|
||||
@@ -248,7 +206,7 @@ function convert_markdown(md_text, dest_dom) {
|
||||
gfm: true
|
||||
};
|
||||
|
||||
var ext = md_plug['pre'];
|
||||
var ext = md_plug.pre;
|
||||
if (ext)
|
||||
Object.assign(marked_opts, ext[0]);
|
||||
|
||||
@@ -349,7 +307,7 @@ function convert_markdown(md_text, dest_dom) {
|
||||
el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
|
||||
}
|
||||
|
||||
ext = md_plug['post'];
|
||||
ext = md_plug.post;
|
||||
if (ext && ext[0].render)
|
||||
try {
|
||||
ext[0].render(md_dom);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html><html><head>
|
||||
<meta charset="utf-8">
|
||||
<title>📝🎉 {{ title }}</title>
|
||||
<title>📝 {{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
{{ html_head }}
|
||||
@@ -25,10 +25,12 @@
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
||||
var last_modified = {{ lastmod }};
|
||||
var last_modified = {{ lastmod }},
|
||||
have_emp = {{ have_emp|tojson }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
link_md_as_html: false,
|
||||
allow_plugins: {{ md_plug }},
|
||||
modpoll_freq: {{ md_chk_rate }}
|
||||
};
|
||||
|
||||
|
||||
@@ -97,7 +97,9 @@
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
||||
var lang="{{ this.args.lang }}";
|
||||
var lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
document.documentElement.className=localStorage.theme||"{{ this.args.theme }}";
|
||||
|
||||
</script>
|
||||
|
||||
@@ -190,6 +190,18 @@ html.y #tth {
|
||||
color: #000;
|
||||
background: #fff;
|
||||
}
|
||||
#cf_frame {
|
||||
position: fixed;
|
||||
z-index: 573;
|
||||
top: 3em;
|
||||
left: 50%;
|
||||
width: 40em;
|
||||
height: 30em;
|
||||
margin-left: -20.2em;
|
||||
border-radius: .4em;
|
||||
border: .4em solid var(--fg);
|
||||
box-shadow: 0 2em 4em 1em var(--bg-max);
|
||||
}
|
||||
#modal {
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
@@ -380,11 +392,13 @@ html.y textarea:focus {
|
||||
padding-left: 2em;
|
||||
border-left: .3em solid #ddd;
|
||||
}
|
||||
.mdo ul>li,
|
||||
.mdo ol>li {
|
||||
.mdo ul>li {
|
||||
margin: .7em 0;
|
||||
list-style-type: disc;
|
||||
}
|
||||
.mdo ol>li {
|
||||
margin: .7em 0 .7em 2em;
|
||||
}
|
||||
.mdo strong {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@@ -1062,6 +1062,7 @@ function up2k_init(subtle) {
|
||||
pvis.drawcard("q");
|
||||
pvis.changecard(pvis.act);
|
||||
}
|
||||
ebi('u2tabw').className = 'ye';
|
||||
}
|
||||
|
||||
function more_one_file() {
|
||||
@@ -2138,8 +2139,8 @@ favico.init();
|
||||
ebi('ico1').onclick = function () {
|
||||
var a = favico.txt == this.textContent;
|
||||
swrite('icot', a ? 'c' : this.textContent);
|
||||
swrite('icof', a ? null : '000');
|
||||
swrite('icob', a ? null : '');
|
||||
swrite('icof', a ? 'fc5' : '000');
|
||||
swrite('icob', a ? '222' : '');
|
||||
favico.init();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ if (!window['console'])
|
||||
};
|
||||
|
||||
|
||||
var is_touch = 'ontouchstart' in window,
|
||||
var wah = '',
|
||||
is_touch = 'ontouchstart' in window,
|
||||
is_https = (window.location + '').indexOf('https:') === 0,
|
||||
IPHONE = is_touch && /iPhone|iPad|iPod/i.test(navigator.userAgent),
|
||||
WINDOWS = navigator.platform ? navigator.platform == 'Win32' : /Windows/.test(navigator.userAgent);
|
||||
@@ -85,15 +86,18 @@ catch (ex) {
|
||||
}
|
||||
var crashed = false, ignexd = {};
|
||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
if ((msg + '').indexOf('ResizeObserver') !== -1)
|
||||
if ((msg + '').indexOf('ResizeObserver') + 1)
|
||||
return; // chrome issue 809574 (benign, from <video>)
|
||||
|
||||
if ((msg + '').indexOf('l2d.js') !== -1)
|
||||
if ((msg + '').indexOf('l2d.js') + 1)
|
||||
return; // `t` undefined in tapEvent -> hitTestSimpleCustom
|
||||
|
||||
if (!/\.js($|\?)/.exec('' + url))
|
||||
return; // chrome debugger
|
||||
|
||||
if ((url + '').indexOf(' > eval') + 1)
|
||||
return; // md timer
|
||||
|
||||
var ekey = url + '\n' + lineNo + '\n' + msg;
|
||||
if (ignexd[ekey] || crashed)
|
||||
return;
|
||||
@@ -1355,6 +1359,49 @@ if (ebi('repl'))
|
||||
ebi('repl').onclick = repl;
|
||||
|
||||
|
||||
var md_plug = {};
|
||||
var md_plug_err = function (ex, js) {
|
||||
if (ex)
|
||||
console.log(ex, js);
|
||||
};
|
||||
function load_md_plug(md_text, plug_type) {
|
||||
if (!have_emp)
|
||||
return md_text;
|
||||
|
||||
var find = '\n```copyparty_' + plug_type + '\n';
|
||||
var ofs = md_text.indexOf(find);
|
||||
if (ofs === -1)
|
||||
return md_text;
|
||||
|
||||
var ofs2 = md_text.indexOf('\n```', ofs + 1);
|
||||
if (ofs2 == -1)
|
||||
return md_text;
|
||||
|
||||
var js = md_text.slice(ofs + find.length, ofs2 + 1);
|
||||
var md = md_text.slice(0, ofs + 1) + md_text.slice(ofs2 + 4);
|
||||
|
||||
var old_plug = md_plug[plug_type];
|
||||
if (!old_plug || old_plug[1] != js) {
|
||||
js = 'const x = { ' + js + ' }; x;';
|
||||
try {
|
||||
var x = eval(js);
|
||||
if (x['ctor']) {
|
||||
x['ctor']();
|
||||
delete x['ctor'];
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
md_plug[plug_type] = null;
|
||||
md_plug_err(ex, js);
|
||||
return md;
|
||||
}
|
||||
md_plug[plug_type] = [x, js];
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
|
||||
var svg_decl = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
|
||||
|
||||
@@ -1380,12 +1427,24 @@ var favico = (function () {
|
||||
var b64;
|
||||
try {
|
||||
b64 = btoa(svg ? svg_decl + svg : gx(r.txt));
|
||||
//console.log('f1');
|
||||
}
|
||||
catch (ex) {
|
||||
b64 = encodeURIComponent(r.txt).replace(/%([0-9A-F]{2})/g,
|
||||
function x(m, v) { return String.fromCharCode('0x' + v); });
|
||||
|
||||
b64 = btoa(gx(unescape(encodeURIComponent(r.txt))));
|
||||
catch (e1) {
|
||||
try {
|
||||
b64 = btoa(gx(encodeURIComponent(r.txt).replace(/%([0-9A-F]{2})/g,
|
||||
function x(m, v) { return String.fromCharCode('0x' + v); })));
|
||||
//console.log('f2');
|
||||
}
|
||||
catch (e2) {
|
||||
try {
|
||||
b64 = btoa(gx(unescape(encodeURIComponent(r.txt))));
|
||||
//console.log('f3');
|
||||
}
|
||||
catch (e3) {
|
||||
//console.log('fe');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!r.tag) {
|
||||
@@ -1398,9 +1457,13 @@ var favico = (function () {
|
||||
|
||||
r.init = function () {
|
||||
clearTimeout(r.to);
|
||||
scfg_bind(r, 'txt', 'icot', '', r.upd);
|
||||
scfg_bind(r, 'fg', 'icof', 'fc5', r.upd);
|
||||
scfg_bind(r, 'bg', 'icob', '222', r.upd);
|
||||
var dv = (window.dfavico || '').trim().split(/ +/),
|
||||
fg = dv.length < 2 ? 'fc5' : dv[1].toLowerCase() == 'none' ? '' : dv[1],
|
||||
bg = dv.length < 3 ? '222' : dv[2].toLowerCase() == 'none' ? '' : dv[2];
|
||||
|
||||
scfg_bind(r, 'txt', 'icot', dv[0], r.upd);
|
||||
scfg_bind(r, 'fg', 'icof', fg, r.upd);
|
||||
scfg_bind(r, 'bg', 'icob', bg, r.upd);
|
||||
r.upd();
|
||||
};
|
||||
|
||||
@@ -1409,6 +1472,7 @@ var favico = (function () {
|
||||
})();
|
||||
|
||||
|
||||
var cf_cha_t = 0;
|
||||
function xhrchk(xhr, prefix, e404) {
|
||||
if (xhr.status < 400 && xhr.status >= 200)
|
||||
return true;
|
||||
@@ -1419,6 +1483,24 @@ function xhrchk(xhr, prefix, e404) {
|
||||
if (xhr.status == 404)
|
||||
return toast.err(0, prefix + e404);
|
||||
|
||||
return toast.err(0, prefix + xhr.status + ": " + (
|
||||
(xhr.response && xhr.response.err) || xhr.responseText));
|
||||
var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
|
||||
fun = toast.err;
|
||||
|
||||
if (xhr.status == 503 && /\bDD(?:wah){0}[o]S [Pp]rote[c]tion|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser/.test(errtxt)) {
|
||||
var now = Date.now(), td = now - cf_cha_t;
|
||||
if (td < 15000)
|
||||
return;
|
||||
|
||||
cf_cha_t = now;
|
||||
errtxt = 'Cloudflare DD' + wah + 'oS protection kicked in\n\n<strong>trying to fix it...</strong>';
|
||||
fun = toast.warn;
|
||||
|
||||
qsr('#cf_frame');
|
||||
var fr = mknod('iframe');
|
||||
fr.src = '/?cf_challenge';
|
||||
fr.setAttribute('id', 'cf_frame');
|
||||
document.body.appendChild(fr);
|
||||
}
|
||||
|
||||
return fun(0, prefix + xhr.status + ": " + errtxt);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
# other stuff
|
||||
|
||||
## [`changelog.md`](changelog.md)
|
||||
* occasionally grabbed from github release notes
|
||||
|
||||
## [`rclone.md`](rclone.md)
|
||||
* notes on using rclone as a fuse client/server
|
||||
|
||||
|
||||
2626
docs/changelog.md
Normal file
2626
docs/changelog.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,9 @@ git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --
|
||||
# download all sfx versions
|
||||
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | tr -d '\r' | while read v t; do fn="$(printf '%s\n' "copyparty $v $t.py" | tr / -)"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
|
||||
|
||||
# convert releasenotes to changelog
|
||||
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \n# \(.created_at) `\(.tag_name)` \(.name)\n\n\(.body)\n\n\n"' | sed -r 's/^# ([0-9]{4}-)([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z /# \1\2\3-\4\5 /' > changelog.md
|
||||
|
||||
# push to multiple git remotes
|
||||
git config -l | grep '^remote'
|
||||
git remote add all git@github.com:9001/copyparty.git
|
||||
|
||||
@@ -4,7 +4,7 @@ ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
||||
ver_hashwasm=4.9.0 \
|
||||
ver_marked=4.0.17 \
|
||||
ver_mde=2.16.1 \
|
||||
ver_codemirror=5.65.5 \
|
||||
ver_codemirror=5.65.6 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
ver_zopfli=1.0.3
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ copyparty/web/browser.html,
|
||||
copyparty/web/browser.js,
|
||||
copyparty/web/browser2.html,
|
||||
copyparty/web/copyparty.gif,
|
||||
copyparty/web/cf.html,
|
||||
copyparty/web/dd,
|
||||
copyparty/web/dd/2.png,
|
||||
copyparty/web/dd/3.png,
|
||||
|
||||
Reference in New Issue
Block a user