mirror of
https://github.com/9001/copyparty.git
synced 2025-10-24 16:43:55 +00:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eacbcda8e5 | ||
|
|
59be76cd44 | ||
|
|
5bb0e7e8b3 | ||
|
|
b78d207121 | ||
|
|
0fcbcdd08c | ||
|
|
ed6c683922 | ||
|
|
9fe1edb02b | ||
|
|
fb3811a708 | ||
|
|
18f8658eec | ||
|
|
3ead4676b0 | ||
|
|
d30001d23d | ||
|
|
06bbf0d656 | ||
|
|
6ddd952e04 | ||
|
|
027ad0c3ee | ||
|
|
3abad2b87b | ||
|
|
32a1c7c5d5 | ||
|
|
f06e165bd4 | ||
|
|
1c843b24f7 | ||
|
|
2ace9ed380 | ||
|
|
5f30c0ae03 | ||
|
|
ef60adf7e2 | ||
|
|
7354b462e8 | ||
|
|
da904d6be8 | ||
|
|
c5fbbbbb5c | ||
|
|
5010387d8a | ||
|
|
f00c54a7fb | ||
|
|
9f52c169d0 | ||
|
|
bf18339404 | ||
|
|
2ad12b074b | ||
|
|
a6788ffe8d | ||
|
|
0e884df486 | ||
|
|
ef1c55286f | ||
|
|
abc0424c26 | ||
|
|
44e5c82e6d | ||
|
|
5849c446ed | ||
|
|
12b7317831 | ||
|
|
fe323f59af | ||
|
|
a00e56f219 | ||
|
|
1a7852794f | ||
|
|
22b1373a57 | ||
|
|
17d78b1469 | ||
|
|
4d8b32b249 | ||
|
|
b65bea2550 | ||
|
|
0b52ccd200 | ||
|
|
3006a07059 | ||
|
|
801dbc7a9a | ||
|
|
4f4e895fb7 | ||
|
|
cc57c3b655 | ||
|
|
ca6ec9c5c7 | ||
|
|
633b1f0a78 | ||
|
|
6136b9bf9c | ||
|
|
524a3ba566 | ||
|
|
58580320f9 | ||
|
|
759b0a994d | ||
|
|
d2800473e4 | ||
|
|
f5b1a2065e | ||
|
|
5e62532295 | ||
|
|
c1bee96c40 | ||
|
|
f273253a2b | ||
|
|
012bbcf770 | ||
|
|
b54cb47b2e | ||
|
|
1b15f43745 | ||
|
|
96771bf1bd | ||
|
|
580078bddb | ||
|
|
c5c7080ec6 | ||
|
|
408339b51d | ||
|
|
02e3d44998 | ||
|
|
156f13ded1 | ||
|
|
d288467cb7 | ||
|
|
21662c9f3f | ||
|
|
9149fe6cdd | ||
|
|
9a146192b7 | ||
|
|
3a9d3b7b61 | ||
|
|
f03f0973ab | ||
|
|
7ec0881e8c | ||
|
|
59e1ab42ff | ||
|
|
722216b901 | ||
|
|
bd8f3dc368 | ||
|
|
33cd94a141 | ||
|
|
053ac74734 | ||
|
|
cced99fafa | ||
|
|
a009ff53f7 | ||
|
|
ca16c4108d | ||
|
|
d1b6c67dc3 | ||
|
|
a61f8133d5 | ||
|
|
38d797a544 | ||
|
|
16c1877f50 | ||
|
|
da5f15a778 | ||
|
|
396c64ecf7 | ||
|
|
252c3a7985 | ||
|
|
a3ecbf0ae7 | ||
|
|
314327d8f2 | ||
|
|
bfacd06929 | ||
|
|
4f5e8f8cf5 | ||
|
|
1fbb4c09cc | ||
|
|
b332e1992b | ||
|
|
5955940b82 | ||
|
|
231a03bcfd | ||
|
|
bc85723657 | ||
|
|
be32b743c6 | ||
|
|
83c9843059 | ||
|
|
11cf43626d | ||
|
|
a6dc5e2ce3 | ||
|
|
38593a0394 | ||
|
|
95309afeea | ||
|
|
c2bf6fe2a3 | ||
|
|
99ac324fbd | ||
|
|
5562de330f | ||
|
|
95014236ac | ||
|
|
6aa7386138 | ||
|
|
3226a1f588 | ||
|
|
b4cf890cd8 | ||
|
|
ce09e323af | ||
|
|
941aedb177 | ||
|
|
87a0d502a3 | ||
|
|
cab7c1b0b8 | ||
|
|
d5892341b6 | ||
|
|
646557a43e | ||
|
|
ed8d34ab43 | ||
|
|
5e34463c77 | ||
|
|
1b14eb7959 | ||
|
|
ed48c2d0ed | ||
|
|
26fe84b660 | ||
|
|
5938230270 | ||
|
|
1a33a047fa |
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: '9001'
|
||||
|
||||
---
|
||||
|
||||
NOTE:
|
||||
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
|
||||
|
||||
|
||||
**Describe the bug**
|
||||
a description of what the bug is
|
||||
|
||||
**To Reproduce**
|
||||
List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it
|
||||
|
||||
**Expected behavior**
|
||||
a description of what you expected to happen
|
||||
|
||||
**Screenshots**
|
||||
if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^)
|
||||
|
||||
**Server details**
|
||||
if the issue is possibly on the server-side, then mention some of the following:
|
||||
* server OS / version:
|
||||
* python version:
|
||||
* copyparty arguments:
|
||||
* filesystem (`lsblk -f` on linux):
|
||||
|
||||
**Client details**
|
||||
if the issue is possibly on the client-side, then mention some of the following:
|
||||
* the device type and model:
|
||||
* OS version:
|
||||
* browser version:
|
||||
|
||||
**Additional context**
|
||||
any other context about the problem here
|
||||
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: '9001'
|
||||
|
||||
---
|
||||
|
||||
all of the below are optional, consider them as inspiration, delete and rewrite at will
|
||||
|
||||
**is your feature request related to a problem? Please describe.**
|
||||
a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]`
|
||||
|
||||
**Describe the idea / solution you'd like**
|
||||
a description of what you want to happen
|
||||
|
||||
**Describe any alternatives you've considered**
|
||||
a description of any alternative solutions or features you've considered
|
||||
|
||||
**Additional context**
|
||||
add any other context or screenshots about the feature request here
|
||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Something else
|
||||
about: "┐(゚∀゚)┌"
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
7
.github/branch-rename.md
vendored
Normal file
7
.github/branch-rename.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
modernize your local checkout of the repo like so,
|
||||
```sh
|
||||
git branch -m master hovudstraum
|
||||
git fetch origin
|
||||
git branch -u origin/hovudstraum hovudstraum
|
||||
git remote set-head origin -a
|
||||
```
|
||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -17,7 +17,7 @@
|
||||
"-mtp",
|
||||
".bpm=f,bin/mtag/audio-bpm.py",
|
||||
"-aed:wark",
|
||||
"-vsrv::r:aed:cnodupe",
|
||||
"-vsrv::r:rw,ed:c,dupe",
|
||||
"-vdist:dist:r"
|
||||
]
|
||||
},
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -55,4 +55,5 @@
|
||||
"py27"
|
||||
],
|
||||
"python.linting.enabled": true,
|
||||
"python.pythonPath": "/usr/bin/python3"
|
||||
}
|
||||
24
CODE_OF_CONDUCT.md
Normal file
24
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,24 @@
|
||||
in the words of Abraham Lincoln:
|
||||
|
||||
> Be excellent to each other... and... PARTY ON, DUDES!
|
||||
|
||||
more specifically I'll paraphrase some examples from a german automotive corporation as they cover all the bases without being too wordy
|
||||
|
||||
## Examples of unacceptable behavior
|
||||
* intimidation, harassment, trolling
|
||||
* insulting, derogatory, harmful or prejudicial comments
|
||||
* posting private information without permission
|
||||
* political or personal attacks
|
||||
|
||||
## Examples of expected behavior
|
||||
* being nice, friendly, welcoming, inclusive, mindful and empathetic
|
||||
* acting considerate, modest, respectful
|
||||
* using polite and inclusive language
|
||||
* criticize constructively and accept constructive criticism
|
||||
* respect different points of view
|
||||
|
||||
## finally and even more specifically,
|
||||
* parse opinions and feedback objectively without prejudice
|
||||
* it's the message that matters, not who said it
|
||||
|
||||
aaand that's how you say `be nice` in a way that fills half a floppy w
|
||||
3
CONTRIBUTING.md
Normal file
3
CONTRIBUTING.md
Normal file
@@ -0,0 +1,3 @@
|
||||
* do something cool
|
||||
|
||||
really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight
|
||||
296
README.md
296
README.md
@@ -8,52 +8,59 @@
|
||||
|
||||
turn your phone or raspi into a portable file server with resumable uploads/downloads using *any* web browser
|
||||
|
||||
* server runs on anything with `py2.7` or `py3.3+`
|
||||
* 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`
|
||||
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [thumbnails](#thumbnails) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer) // [ie4](#browser-support)
|
||||
|
||||
|
||||
## readme toc
|
||||
|
||||
* top
|
||||
* [quickstart](#quickstart) - download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||
* **[quickstart](#quickstart)** - download **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** and you're all set!
|
||||
* [on servers](#on-servers) - you may also want these, especially on servers
|
||||
* [on debian](#on-debian) - recommended additional steps on debian
|
||||
* [notes](#notes) - general notes
|
||||
* [status](#status) - summary: all planned features work! now please enjoy the bloatening
|
||||
* [status](#status) - feature summary
|
||||
* [testimonials](#testimonials) - small collection of user feedback
|
||||
* [motivations](#motivations) - project goals / philosophy
|
||||
* [future plans](#future-plans) - some improvement ideas
|
||||
* [bugs](#bugs)
|
||||
* [general bugs](#general-bugs)
|
||||
* [not my bugs](#not-my-bugs)
|
||||
* [FAQ](#FAQ) - "frequently" asked questions
|
||||
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
|
||||
* [the browser](#the-browser) - accessing a copyparty server using a web-browser
|
||||
* [tabs](#tabs) - the main tabs in the ui
|
||||
* [hotkeys](#hotkeys) - the browser has the following hotkeys (always qwerty)
|
||||
* [hotkeys](#hotkeys) - the browser has the following hotkeys
|
||||
* [navpane](#navpane) - switching between breadcrumbs or navpane
|
||||
* [thumbnails](#thumbnails) - press `g` to toggle image/video thumbnails instead of the file listing
|
||||
* [thumbnails](#thumbnails) - press `g` 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) - web-browsers can upload using `bup` and `up2k`
|
||||
* [file-search](#file-search) - drop files/folders into up2k to see if they exist on the server
|
||||
* [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
|
||||
* [unpost](#unpost) - undo/delete accidental uploads
|
||||
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
|
||||
* [batch rename](#batch-rename) - select some files and press F2 to bring up the rename UI
|
||||
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
|
||||
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
||||
* [other tricks](#other-tricks)
|
||||
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
||||
* [server config](#server-config)
|
||||
* [file indexing](#file-indexing)
|
||||
* [upload rules](#upload-rules) - set upload rules using volume flags, some examples
|
||||
* [upload rules](#upload-rules) - set upload rules using volume flags
|
||||
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
|
||||
* [database location](#database-location) - can be stored in-volume (default) or elsewhere
|
||||
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
|
||||
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
|
||||
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
||||
* [complete examples](#complete-examples)
|
||||
* [browser support](#browser-support) - TLDR: yes
|
||||
* [client examples](#client-examples) - interact with copyparty using non-browser clients
|
||||
* [up2k](#up2k) - quick outline of the up2k protocol, see [uploading](#uploading) for the web-client
|
||||
* [performance](#performance) - defaults are good for most cases
|
||||
* [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right?
|
||||
* [performance](#performance) - defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||
* [security](#security) - some notes on hardening
|
||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||
* [dependencies](#dependencies) - mandatory deps
|
||||
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
|
||||
* [install recommended deps](#install-recommended-deps)
|
||||
@@ -71,28 +78,31 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
|
||||
## quickstart
|
||||
|
||||
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||
download **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** and you're all set!
|
||||
|
||||
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want [accounts and volumes](#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; see `-h` for help if you want [accounts and volumes](#accounts-and-volumes) etc
|
||||
|
||||
some recommended options:
|
||||
* `-e2dsa` enables general file indexing, see [search configuration](#search-configuration)
|
||||
* `-e2dsa` enables general [file indexing](#file-indexing)
|
||||
* `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen), see [optional dependencies](#optional-dependencies)
|
||||
* `-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 access levels (`r`ead, `w`rite, `m`ove, `d`elete)
|
||||
* 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
|
||||
|
||||
|
||||
### on servers
|
||||
|
||||
you may also want these, especially on servers:
|
||||
|
||||
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service
|
||||
* [contrib/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security)
|
||||
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for better https)
|
||||
|
||||
|
||||
### on debian
|
||||
|
||||
recommended additional steps on debian
|
||||
|
||||
enable audio metadata and thumbnails (from images and videos):
|
||||
recommended additional steps on debian which enable audio metadata and thumbnails (from images and videos):
|
||||
|
||||
* as root, run the following:
|
||||
`apt install python3 python3-pip python3-dev ffmpeg`
|
||||
@@ -114,12 +124,12 @@ browser-specific:
|
||||
* Android-Chrome: increase "parallel uploads" for higher speed (android bug)
|
||||
* Android-Firefox: takes a while to select files (their fix for ☝️)
|
||||
* Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now*
|
||||
* Desktop-Firefox: may stop you from deleting folders you've uploaded until you visit `about:memory` and click `Minimize memory usage`
|
||||
* Desktop-Firefox: may stop you from deleting files you've uploaded until you visit `about:memory` and click `Minimize memory usage`
|
||||
|
||||
|
||||
## status
|
||||
|
||||
summary: all planned features work! now please enjoy the bloatening
|
||||
feature summary
|
||||
|
||||
* backend stuff
|
||||
* ☑ sanic multipart parser
|
||||
@@ -137,7 +147,7 @@ summary: all planned features work! now please enjoy the bloatening
|
||||
* ☑ [folders as zip / tar files](#zip-downloads)
|
||||
* ☑ FUSE client (read-only)
|
||||
* browser
|
||||
* ☑ navpane (directory tree sidebar)
|
||||
* ☑ [navpane](#navpane) (directory tree sidebar)
|
||||
* ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))
|
||||
* ☑ audio player (with OS media controls)
|
||||
* ☑ image gallery with webm player
|
||||
@@ -163,17 +173,51 @@ small collection of user feedback
|
||||
`good enough`, `surprisingly correct`, `certified good software`, `just works`, `why`
|
||||
|
||||
|
||||
# motivations
|
||||
|
||||
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
|
||||
* 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
|
||||
* be a good emergency solution for transferring stuff between ancient boxes
|
||||
* minimal dependencies
|
||||
* but optional dependencies adding bonus-features are ok
|
||||
* everything being plaintext makes it possible to proofread for malicious code
|
||||
* no preparations / setup necessary, just run the sfx (which is also plaintext)
|
||||
* adaptable, malleable, hackable
|
||||
* no build steps; modify the js/python without needing node.js or anything like that
|
||||
|
||||
|
||||
## future plans
|
||||
|
||||
some improvement ideas
|
||||
|
||||
* the JS is a mess -- a preact rewrite would be nice
|
||||
* preferably without build dependencies like webpack/babel/node.js, maybe a python thing to assemble js files into main.js
|
||||
* good excuse to look at using virtual lists (browsers start to struggle when folders contain over 5000 files)
|
||||
* the UX is a mess -- a proper design would be nice
|
||||
* very organic (much like the python/js), everything was an afterthought
|
||||
* true for both the layout and the visual flair
|
||||
* something like the tron board-room ui (or most other hollywood ones, like ironman) would be :100:
|
||||
* some of the python files are way too big
|
||||
* `up2k.py` ended up doing all the file indexing / db management
|
||||
* `httpcli.py` should be separated into modules in general
|
||||
|
||||
|
||||
# bugs
|
||||
|
||||
* Windows: python 3.7 and older cannot read tags with FFprobe, so use Mutagen or upgrade
|
||||
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
||||
* Windows: python 2.7 cannot handle filenames with mojibake
|
||||
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions
|
||||
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions (macos, some linux)
|
||||
|
||||
## general bugs
|
||||
|
||||
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
||||
* cannot mount something at `/d1/d2/d3` unless `d2` exists inside `d1`
|
||||
* probably more, pls let me know
|
||||
|
||||
## not my bugs
|
||||
@@ -188,21 +232,34 @@ small collection of user feedback
|
||||
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db inside the vm instead
|
||||
|
||||
|
||||
# FAQ
|
||||
|
||||
"frequently" asked questions
|
||||
|
||||
* is it possible to block read-access to folders unless you know the exact URL for a particular file inside?
|
||||
* yes, using the [`g` permission](#accounts-and-volumes), see the examples there
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
# accounts and volumes
|
||||
|
||||
per-folder, per-user permissions
|
||||
* `-a usr:pwd` adds account `usr` with password `pwd`
|
||||
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
|
||||
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||
* when granting permissions to an account, the names are comma-separated: `-v .::r,usr1,usr2:rw,usr3,usr4`
|
||||
* granting the same permissions to multiple accounts:
|
||||
`-v .::r,usr1,usr2:rw,usr3,usr4` = usr1/2 read-only, 3/4 read-write
|
||||
|
||||
permissions:
|
||||
* `r` (read): browse folder contents, download files, download as zip/tar
|
||||
* `w` (write): upload files, move files *into* folder
|
||||
* `m` (move): move files/folders *from* folder
|
||||
* `w` (write): upload files, move files *into* this folder
|
||||
* `m` (move): move files/folders *from* this folder
|
||||
* `d` (delete): delete files/folders
|
||||
* `g` (get): only download files, cannot see folder contents or zip/tar
|
||||
|
||||
example:
|
||||
examples:
|
||||
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
||||
* make folder `/srv` the root of the filesystem, read-only by anyone: `-v /srv::r`
|
||||
* make folder `/mnt/music` available at `/music`, read-only for u1 and u2, read-write for u3: `-v /mnt/music:music:r,u1,u2:rw,u3`
|
||||
@@ -211,6 +268,10 @@ example:
|
||||
* unauthorized users accessing the webroot can see that the `inc` folder exists, but cannot open it
|
||||
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
|
||||
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
|
||||
* make folder `/mnt/ss` available at `/i`, read-write for u1, get-only for everyone else, and enable accesskeys: `-v /mnt/ss:i:rw,u1:g:c,fk=4`
|
||||
* `c,fk=4` sets the `fk` volume-flag to 4, meaning each file gets a 4-character accesskey
|
||||
* `u1` can upload files, browse the folder, and see the generated accesskeys
|
||||
* other users cannot browse the folder, but can access the files if they have the full file URL with the accesskey
|
||||
|
||||
|
||||
# the browser
|
||||
@@ -235,15 +296,15 @@ the main tabs in the ui
|
||||
|
||||
## hotkeys
|
||||
|
||||
the browser has the following hotkeys (always qwerty)
|
||||
* `B` toggle breadcrumbs / navpane
|
||||
the browser has the following hotkeys (always qwerty)
|
||||
* `B` toggle breadcrumbs / [navpane](#navpane)
|
||||
* `I/K` prev/next folder
|
||||
* `M` parent folder (or unexpand current)
|
||||
* `G` toggle list / grid view
|
||||
* `G` toggle list / [grid view](#thumbnails)
|
||||
* `T` toggle thumbnails / icons
|
||||
* `ctrl-X` cut selected files/folders
|
||||
* `ctrl-V` paste
|
||||
* `F2` rename selected file/folder
|
||||
* `F2` [rename](#batch-rename) selected file/folder
|
||||
* when a file/folder is selected (in not-grid-view):
|
||||
* `Up/Down` move cursor
|
||||
* shift+`Up/Down` select and move cursor
|
||||
@@ -270,7 +331,7 @@ the browser has the following hotkeys (always qwerty)
|
||||
* `M` mute
|
||||
* when the navpane is open:
|
||||
* `A/D` adjust tree width
|
||||
* in the grid view:
|
||||
* in the [grid view](#thumbnails):
|
||||
* `S` toggle multiselect
|
||||
* shift+`A/D` zoom
|
||||
* in the markdown editor:
|
||||
@@ -288,12 +349,14 @@ switching between breadcrumbs or navpane
|
||||
|
||||
click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (default), or a navpane (tree-browser sidebar thing)
|
||||
|
||||
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
||||
* `[-]` and `[+]` (or hotkeys `A`/`D`) adjust the size
|
||||
* `[v]` jumps to the currently open folder
|
||||
* `[a]` toggles automatic widening as you go deeper
|
||||
|
||||
|
||||
## thumbnails
|
||||
|
||||
press `g` to toggle image/video thumbnails instead of the file listing
|
||||
press `g` to toggle grid-view instead of the file listing, and `t` toggles icons / thumbnails
|
||||
|
||||

|
||||
|
||||
@@ -308,7 +371,7 @@ in the grid/thumbnail view, if the audio player panel is open, songs will start
|
||||
|
||||
download folders (or file selections) as `zip` or `tar` files
|
||||
|
||||
select which type of archive you want in the browser settings tab:
|
||||
select which type of archive you want in the `[⚙️] config` tab:
|
||||
|
||||
| name | url-suffix | description |
|
||||
|--|--|--|
|
||||
@@ -330,11 +393,13 @@ you can also zip a selection of files or folders by clicking them in the browser
|
||||
|
||||
## uploading
|
||||
|
||||
web-browsers can upload using `bup` and `up2k`:
|
||||
drag files/folders into the web-browser to upload
|
||||
|
||||
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
|
||||
|
||||
you can undo/delete uploads using `[🧯]` [unpost](#unpost)
|
||||
you can also undo/delete uploads by using `[🧯]` [unpost](#unpost)
|
||||
|
||||
up2k has several advantages:
|
||||
* you can drop folders into the browser (files are added recursively)
|
||||
@@ -355,36 +420,41 @@ see [up2k](#up2k) for details on how it works
|
||||
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 list
|
||||
* `[💭]` 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 mode
|
||||
* `[🔎]` switch between upload and [file-search](#file-search) mode
|
||||
* ignore `[🔎]` if you add files by dragging them into the browser
|
||||
|
||||
and then theres the tabs below it,
|
||||
* `[ok]` is uploads which completed successfully
|
||||
* `[ng]` is the uploads which failed / got rejected (already exists, ...)
|
||||
* `[ok]` is the files which completed successfully
|
||||
* `[ng]` is the ones that failed / got rejected (already exists, ...)
|
||||
* `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order
|
||||
* `[busy]` files which are currently hashing, pending-upload, or uploading
|
||||
* 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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
### file-search
|
||||
|
||||
drop files/folders into up2k to see if they exist on the server
|
||||
dropping files into the browser also lets you see if they exist on the server
|
||||
|
||||

|
||||
|
||||
in the `[🚀 up2k]` tab, after toggling the `[🔎]` switch green, any files/folders you drop onto the dropzone will be hashed on the client-side. Each hash is sent to the server which checks if that file exists somewhere
|
||||
when you drag/drop files into the browser, you will see two dropzones: `Upload` and `Search`
|
||||
|
||||
> on a phone? toggle the `[🔎]` switch green before tapping the big yellow Search button to select your files
|
||||
|
||||
the files will be hashed on the client-side, and each hash is sent to the server, which checks if that file exists somewhere
|
||||
|
||||
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
|
||||
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
|
||||
|
||||
adding the same file multiple times is blocked, so if you first search for a file and then decide to upload it, you have to click the `[cleanup]` button to discard `[done]` files (or just refresh the page)
|
||||
|
||||
note that since up2k has to read the file twice, `[🎈 bup]` can be up to 2x faster in extreme cases (if your internet connection is faster than the read-speed of your HDD)
|
||||
|
||||
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)
|
||||
|
||||
|
||||
### unpost
|
||||
|
||||
@@ -399,12 +469,22 @@ you can unpost even if you don't have regular move/delete access, however only f
|
||||
|
||||
cut/paste, rename, and delete files/folders (if you have permission)
|
||||
|
||||
file selection: click somewhere on the line (not the link itsef), then:
|
||||
* `space` to toggle
|
||||
* `up/down` to move
|
||||
* `shift-up/down` to move-and-select
|
||||
* `ctrl-shift-up/down` to also scroll
|
||||
|
||||
* cut: select some files and `ctrl-x`
|
||||
* paste: `ctrl-v` in another folder
|
||||
* rename: `F2`
|
||||
|
||||
you can move files across browser tabs (cut in one tab, paste in another)
|
||||
|
||||
|
||||
## batch rename
|
||||
|
||||
select some files and press F2 to bring up the rename UI
|
||||
select some files and press `F2` to bring up the rename UI
|
||||
|
||||

|
||||
|
||||
@@ -464,6 +544,12 @@ and there are *two* editors
|
||||
|
||||
* 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
|
||||
|
||||
* click the bottom-left `π` to open a javascript prompt for debugging
|
||||
|
||||
* files named `.prologue.html` / `.epilogue.html` will be rendered before/after directory listings unless `--no-logues`
|
||||
|
||||
* files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence)
|
||||
|
||||
|
||||
## searching
|
||||
|
||||
@@ -486,26 +572,27 @@ add the argument `-e2ts` to also scan/index tags from music files, which brings
|
||||
|
||||
## file indexing
|
||||
|
||||
file indexing relies on two databases, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`). Configuration can be done through arguments, volume flags, or a mix of both.
|
||||
file indexing relies on two database tables, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`), stored in `.hist/up2k.db`. Configuration can be done through arguments, volume flags, or a mix of both.
|
||||
|
||||
through arguments:
|
||||
* `-e2d` enables file indexing on upload
|
||||
* `-e2ds` scans writable folders for new files on startup
|
||||
* `-e2dsa` scans all mounted volumes (including readonly ones)
|
||||
* `-e2ds` also scans writable folders for new files on startup
|
||||
* `-e2dsa` also scans all mounted volumes (including readonly ones)
|
||||
* `-e2t` enables metadata indexing on upload
|
||||
* `-e2ts` scans for tags in all files that don't have tags yet
|
||||
* `-e2tsr` deletes all existing tags, does a full reindex
|
||||
* `-e2ts` also scans for tags in all files that don't have tags yet
|
||||
* `-e2tsr` also deletes all existing tags, doing a full reindex
|
||||
|
||||
the same arguments can be set as volume flags, in addition to `d2d` and `d2t` for disabling:
|
||||
* `-v ~/music::r:c,e2dsa:c,e2tsr` does a full reindex of everything on startup
|
||||
* `-v ~/music::r:c,e2dsa,e2tsr` does a full reindex of everything on startup
|
||||
* `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on
|
||||
* `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||
|
||||
note:
|
||||
* the parser can finally handle `c,e2dsa,e2tsr` so you no longer have to `c,e2dsa:c,e2tsr`
|
||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
||||
|
||||
you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `:c,dhash`, this has the following consequences:
|
||||
to save some time, you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `:c,dhash`, this has the following consequences:
|
||||
* initial indexing is way faster, especially when the volume is on a network disk
|
||||
* makes it impossible to [file-search](#file-search)
|
||||
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
||||
@@ -515,7 +602,7 @@ if you set `--no-hash`, you can enable hashing for specific volumes using flag `
|
||||
|
||||
## upload rules
|
||||
|
||||
set upload rules using volume flags, some examples:
|
||||
set upload rules using volume flags, some examples:
|
||||
|
||||
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: b, k, m, g)
|
||||
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
||||
@@ -534,9 +621,7 @@ you can also set transaction limits which apply per-IP and per-volume, but these
|
||||
|
||||
## compress uploads
|
||||
|
||||
files can be autocompressed on upload
|
||||
|
||||
compression is either on user-request (if config allows) or forced by server-config
|
||||
files can be autocompressed on upload, either on user-request (if config allows) or forced by server-config
|
||||
|
||||
* volume flag `gz` allows gz compression
|
||||
* volume flag `xz` allows lzma compression
|
||||
@@ -557,7 +642,7 @@ some examples,
|
||||
|
||||
## database location
|
||||
|
||||
can be stored in-volume (default) or elsewhere
|
||||
in-volume (`.hist/up2k.db`, default) or somewhere else
|
||||
|
||||
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
|
||||
|
||||
@@ -579,13 +664,13 @@ set `-e2t` to index tags on upload
|
||||
|
||||
if you add/remove a tag from `mte` you will need to run with `-e2tsr` once to rebuild the database, otherwise only new files will be affected
|
||||
|
||||
but instead of using `-mte`, `-mth` is a better way to hide tags in the browser: these tags will not be displayed by default, but they still get indexed and become searchable, and users can choose to unhide them in the settings pane
|
||||
but instead of using `-mte`, `-mth` is a better way to hide tags in the browser: these tags will not be displayed by default, but they still get indexed and become searchable, and users can choose to unhide them in the `[⚙️] config` pane
|
||||
|
||||
`-mtm` can be used to add or redefine a metadata mapping, say you have media files with `foo` and `bar` tags and you want them to display as `qux` in the browser (preferring `foo` if both are present), then do `-mtm qux=foo,bar` and now you can `-mte artist,title,qux`
|
||||
|
||||
tags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric value
|
||||
|
||||
see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/master/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,)
|
||||
see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,)
|
||||
|
||||
`--no-mutagen` disables Mutagen and uses FFprobe instead, which...
|
||||
* is about 20x slower than Mutagen
|
||||
@@ -641,29 +726,27 @@ TLDR: yes
|
||||
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| video player | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
||||
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
||||
| markdown viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
|
||||
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
||||
|
||||
* internet explorer 6 to 8 behave the same
|
||||
* firefox 52 and chrome 49 are the last winxp versions
|
||||
* `*1` yes, but extremely slow (ie10: 1 MiB/s, ie11: 270 KiB/s)
|
||||
* firefox 52 and chrome 49 are the final winxp versions
|
||||
* `*1` yes, but extremely slow (ie10: `1 MiB/s`, ie11: `270 KiB/s`)
|
||||
* `*2` causes a full-page refresh on each navigation
|
||||
* `*3` using a wasm decoder which can sometimes get stuck and consumes a bit more power
|
||||
* `*3` using a wasm decoder which consumes a bit more power
|
||||
|
||||
quick summary of more eccentric web-browsers trying to view a directory index:
|
||||
|
||||
| browser | will it blend |
|
||||
| ------- | ------------- |
|
||||
| **safari** (14.0.3/macos) | is chrome with janky wasm, so playing opus can deadlock the javascript engine |
|
||||
| **safari** (14.0.1/iOS) | same as macos, except it recovers from the deadlocks if you poke it a bit |
|
||||
| **links** (2.21/macports) | can browse, login, upload/mkdir/msg |
|
||||
| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |
|
||||
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
|
||||
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
|
||||
| **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 (text is yellow on white), upload with `?b=u` |
|
||||
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u` |
|
||||
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
|
||||
|
||||
|
||||
@@ -694,6 +777,8 @@ copyparty returns a truncated sha512sum of your PUT/POST as base64; you can gene
|
||||
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
|
||||
b512 <movie.mkv
|
||||
|
||||
you can provide passwords using cookie 'cppwd=hunter2', as a url query `?pw=hunter2`, or with basic-authentication (either as the username or password)
|
||||
|
||||
|
||||
# up2k
|
||||
|
||||
@@ -710,12 +795,25 @@ quick outline of the up2k protocol, see [uploading](#uploading) for the web-clie
|
||||
* server writes chunks into place based on the hash
|
||||
* client does another handshake with the hashlist; server replies with OK or a list of chunks to reupload
|
||||
|
||||
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)
|
||||
|
||||
|
||||
## why chunk-hashes
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
# performance
|
||||
|
||||
defaults are good for most cases
|
||||
defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||
|
||||
you can ignore the `cannot efficiently use multiple CPU cores` message, it's very unlikely to be a problem
|
||||
you can ignore the `cannot efficiently use multiple CPU cores` message, very unlikely to be a problem
|
||||
|
||||
below are some tweaks roughly ordered by usefulness:
|
||||
|
||||
@@ -730,6 +828,29 @@ below are some tweaks roughly ordered by usefulness:
|
||||
...however it adds an overhead to internal communication so it might be a net loss, see if it works 4 u
|
||||
|
||||
|
||||
# security
|
||||
|
||||
some notes on hardening
|
||||
|
||||
on public copyparty instances with anonymous upload enabled:
|
||||
|
||||
* 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)
|
||||
|
||||
other misc:
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
## gotchas
|
||||
|
||||
behavior that might be unexpected
|
||||
|
||||
* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example
|
||||
|
||||
|
||||
# dependencies
|
||||
|
||||
mandatory deps:
|
||||
@@ -744,17 +865,11 @@ enable music tags:
|
||||
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
||||
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
||||
|
||||
enable thumbnails of images:
|
||||
* `Pillow` (requires py2.7 or py3.5+)
|
||||
|
||||
enable thumbnails of videos:
|
||||
* `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||
|
||||
enable thumbnails of HEIF pictures:
|
||||
* `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||
|
||||
enable thumbnails of AVIF pictures:
|
||||
* `pillow-avif-plugin`
|
||||
enable [thumbnails](#thumbnails) of...
|
||||
* **images:** `Pillow` (requires py2.7 or py3.5+)
|
||||
* **videos:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||
* **HEIF pictures:** `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||
* **AVIF pictures:** `pillow-avif-plugin`
|
||||
|
||||
|
||||
## install recommended deps
|
||||
@@ -791,8 +906,10 @@ if you don't need all the features, you can repack the sfx and save a bunch of s
|
||||
* `223k` after `./scripts/make-sfx.sh re no-ogv no-cm`
|
||||
|
||||
the features you can opt to drop are
|
||||
* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files
|
||||
* `cm`/easymde, the "fancy" markdown editor
|
||||
* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files, saves ~192k
|
||||
* `cm`/easymde, the "fancy" markdown editor, saves ~92k
|
||||
* `fnt`, source-code-pro, the monospace font, saves ~9k
|
||||
* `dd`, the custom mouse cursor for the media player tray tab, saves ~2k
|
||||
|
||||
for the `re`pack to work, first run one of the sfx'es once to unpack it
|
||||
|
||||
@@ -828,7 +945,7 @@ pip install black bandit pylint flake8 # vscode tooling
|
||||
|
||||
## just the sfx
|
||||
|
||||
grab the web-dependencies from a previous sfx (unless you need to modify something in those):
|
||||
first grab the web-dependencies from a previous sfx (assuming you don't need to modify something in those):
|
||||
|
||||
```sh
|
||||
rm -rf copyparty/web/deps
|
||||
@@ -854,8 +971,8 @@ in the `scripts` folder:
|
||||
|
||||
* run `make -C deps-docker` to build all dependencies
|
||||
* `git tag v1.2.3 && git push origin --tags`
|
||||
* create github release with `make-tgz-release.sh`
|
||||
* upload to pypi with `make-pypi-release.(sh|bat)`
|
||||
* create github release with `make-tgz-release.sh`
|
||||
* create sfx with `make-sfx.sh`
|
||||
|
||||
|
||||
@@ -863,8 +980,7 @@ in the `scripts` folder:
|
||||
|
||||
roughly sorted by priority
|
||||
|
||||
* hls framework for Someone Else to drop code into :^)
|
||||
* readme.md as epilogue
|
||||
* nothing! currently
|
||||
|
||||
|
||||
## discarded ideas
|
||||
@@ -892,3 +1008,5 @@ roughly sorted by priority
|
||||
* indexedDB for hashes, cfg enable/clear/sz, 2gb avail, ~9k for 1g, ~4k for 100m, 500k items before autoeviction
|
||||
* blank hashlist when up-ok to skip handshake
|
||||
* too many confusing side-effects
|
||||
* hls framework for Someone Else to drop code into :^)
|
||||
* probably not, too much stuff to consider -- seeking, start at offset, task stitching (probably np-hard), conditional passthru, rate-control (especially multi-consumer), session keepalive, cache mgmt...
|
||||
|
||||
@@ -22,7 +22,7 @@ dependencies:
|
||||
|
||||
note:
|
||||
you probably want to run this on windows clients:
|
||||
https://github.com/9001/copyparty/blob/master/contrib/explorer-nothumbs-nofoldertypes.reg
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/contrib/explorer-nothumbs-nofoldertypes.reg
|
||||
|
||||
get server cert:
|
||||
awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
standalone programs which take an audio file as argument
|
||||
|
||||
**NOTE:** these all require `-e2ts` to be functional, meaning you need to do at least one of these: `apt install ffmpeg` or `pip3 install mutagen`
|
||||
|
||||
some of these rely on libraries which are not MIT-compatible
|
||||
|
||||
* [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2
|
||||
* [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3
|
||||
* [media-hash.py](./media-hash.py) generates checksums for audio and video streams; uses FFmpeg (LGPL or GPL)
|
||||
|
||||
these do not have any problematic dependencies:
|
||||
|
||||
* [exe.py](./exe.py) grabs metadata from .exe and .dll files (example for retrieving multiple tags with one parser)
|
||||
* [wget.py](./wget.py) lets you download files by POSTing URLs to copyparty
|
||||
|
||||
|
||||
# dependencies
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ def det(tf):
|
||||
"-v", "fatal",
|
||||
"-ss", "13",
|
||||
"-y", "-i", fsenc(sys.argv[1]),
|
||||
"-map", "0:a:0",
|
||||
"-ac", "1",
|
||||
"-ar", "22050",
|
||||
"-t", "300",
|
||||
|
||||
@@ -28,6 +28,7 @@ def det(tf):
|
||||
"-hide_banner",
|
||||
"-v", "fatal",
|
||||
"-y", "-i", fsenc(sys.argv[1]),
|
||||
"-map", "0:a:0",
|
||||
"-t", "300",
|
||||
"-sample_fmt", "s16",
|
||||
tf
|
||||
|
||||
@@ -4,7 +4,8 @@ set -e
|
||||
|
||||
# install dependencies for audio-*.py
|
||||
#
|
||||
# linux: requires {python3,ffmpeg,fftw}-dev py3-{wheel,pip} py3-numpy{,-dev} vamp-sdk-dev patchelf
|
||||
# linux/alpine: requires {python3,ffmpeg,fftw}-dev py3-{wheel,pip} py3-numpy{,-dev} vamp-sdk-dev patchelf cmake
|
||||
# linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake
|
||||
# win64: requires msys2-mingw64 environment
|
||||
# macos: requires macports
|
||||
#
|
||||
|
||||
85
bin/mtag/wget.py
Normal file
85
bin/mtag/wget.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
use copyparty as a file downloader by POSTing URLs as
|
||||
application/x-www-form-urlencoded (for example using the
|
||||
message/pager function on the website)
|
||||
|
||||
example copyparty config to use this:
|
||||
--urlform save,get -vsrv/wget:wget:rwmd,ed:c,e2ts,mtp=title=ebin,t300,ad,bin/mtag/wget.py
|
||||
|
||||
explained:
|
||||
for realpath srv/wget (served at /wget) with read-write-modify-delete for ed,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/wget.py" to provide metadata tag "title",
|
||||
do this on all uploads with the file extension "bin",
|
||||
t300 = 300 seconds timeout for each dwonload,
|
||||
ad = parse file regardless if FFmpeg thinks it is audio or not
|
||||
|
||||
PS: this requires e2ts to be functional,
|
||||
meaning you need to do at least one of these:
|
||||
* apt install ffmpeg
|
||||
* pip3 install mutagen
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess as sp
|
||||
from urllib.parse import unquote_to_bytes as unquote
|
||||
|
||||
|
||||
def main():
|
||||
fp = os.path.abspath(sys.argv[1])
|
||||
fdir = os.path.dirname(fp)
|
||||
fname = os.path.basename(fp)
|
||||
if not fname.startswith("put-") or not fname.endswith(".bin"):
|
||||
raise Exception("not a post file")
|
||||
|
||||
buf = b""
|
||||
with open(fp, "rb") as f:
|
||||
while True:
|
||||
b = f.read(4096)
|
||||
buf += b
|
||||
if len(buf) > 4096:
|
||||
raise Exception("too big")
|
||||
|
||||
if not b:
|
||||
break
|
||||
|
||||
if not buf:
|
||||
raise Exception("file is empty")
|
||||
|
||||
buf = unquote(buf.replace(b"+", b" "))
|
||||
url = buf.decode("utf-8")
|
||||
|
||||
if not url.startswith("msg="):
|
||||
raise Exception("does not start with msg=")
|
||||
|
||||
url = url[4:]
|
||||
if "://" not in url:
|
||||
url = "https://" + url
|
||||
|
||||
os.chdir(fdir)
|
||||
|
||||
name = url.split("?")[0].split("/")[-1]
|
||||
tfn = "-- DOWNLOADING " + name
|
||||
open(tfn, "wb").close()
|
||||
|
||||
cmd = ["wget", "--trust-server-names", "--", url]
|
||||
|
||||
try:
|
||||
sp.check_call(cmd)
|
||||
|
||||
# OPTIONAL:
|
||||
# on success, delete the .bin file which contains the URL
|
||||
os.unlink(fp)
|
||||
except:
|
||||
open("-- FAILED TO DONWLOAD " + name, "wb").close()
|
||||
|
||||
os.unlink(tfn)
|
||||
print(url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -17,7 +17,7 @@ it's probably best to use this through a config file; see res/yt-ipr.conf
|
||||
|
||||
but if you want to use plain arguments instead then:
|
||||
-v srv/ytm:ytm:w:rw,ed
|
||||
:c,e2ts:c,e2dsa
|
||||
:c,e2ts,e2dsa
|
||||
:c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H
|
||||
:c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||
:c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||
|
||||
@@ -29,7 +29,8 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share
|
||||
|
||||
# OS integration
|
||||
init-scripts to start copyparty as a service
|
||||
* [`systemd/copyparty.service`](systemd/copyparty.service)
|
||||
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally
|
||||
* [`systemd/prisonparty.service`](systemd/prisonparty.service) runs the sfx in a chroot
|
||||
* [`openrc/copyparty`](openrc/copyparty)
|
||||
|
||||
# Reverse-proxy
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# ca-name and server-name
|
||||
# ca-name and server-fqdn
|
||||
ca_name="$1"
|
||||
srv_name="$2"
|
||||
srv_fqdn="$2"
|
||||
|
||||
[ -z "$srv_name" ] && {
|
||||
[ -z "$srv_fqdn" ] && {
|
||||
echo "need arg 1: ca name"
|
||||
echo "need arg 2: server name"
|
||||
echo "need arg 2: server fqdn"
|
||||
echo "optional arg 3: if set, write cert into copyparty cfg"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -31,15 +32,15 @@ EOF
|
||||
gen_srv() {
|
||||
(tee /dev/stderr <<EOF
|
||||
{"key": {"algo":"rsa", "size":4096},
|
||||
"names": [{"O":"$ca_name - $srv_name"}]}
|
||||
"names": [{"O":"$ca_name - $srv_fqdn"}]}
|
||||
EOF
|
||||
)|
|
||||
cfssl gencert -ca ca.pem -ca-key ca.key \
|
||||
-profile=www -hostname="$srv_name.$ca_name" - |
|
||||
cfssljson -bare "$srv_name"
|
||||
-profile=www -hostname="$srv_fqdn" - |
|
||||
cfssljson -bare "$srv_fqdn"
|
||||
|
||||
mv "$srv_name-key.pem" "$srv_name.key"
|
||||
rm "$srv_name.csr"
|
||||
mv "$srv_fqdn-key.pem" "$srv_fqdn.key"
|
||||
rm "$srv_fqdn.csr"
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +58,13 @@ show() {
|
||||
awk '!o; {o=0} /[0-9a-f:]{16}/{o=1}'
|
||||
}
|
||||
show ca.pem
|
||||
show "$srv_name.pem"
|
||||
show "$srv_fqdn.pem"
|
||||
|
||||
|
||||
# write cert into copyparty config
|
||||
[ -z "$3" ] || {
|
||||
mkdir -p ~/.config/copyparty
|
||||
cat "$srv_name".{key,pem} ca.pem >~/.config/copyparty/cert.pem
|
||||
cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
@@ -104,7 +104,7 @@ def ensure_cert():
|
||||
cert_insec = os.path.join(E.mod, "res/insecure.pem")
|
||||
cert_cfg = os.path.join(E.cfg, "cert.pem")
|
||||
if not os.path.exists(cert_cfg):
|
||||
shutil.copy2(cert_insec, cert_cfg)
|
||||
shutil.copy(cert_insec, cert_cfg)
|
||||
|
||||
try:
|
||||
if filecmp.cmp(cert_cfg, cert_insec):
|
||||
@@ -203,6 +203,11 @@ def run_argparse(argv, formatter):
|
||||
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
||||
)
|
||||
|
||||
try:
|
||||
fk_salt = unicode(os.path.getmtime(os.path.join(E.cfg, "cert.pem")))
|
||||
except:
|
||||
fk_salt = "hunter2"
|
||||
|
||||
sects = [
|
||||
[
|
||||
"accounts",
|
||||
@@ -211,14 +216,15 @@ def run_argparse(argv, formatter):
|
||||
"""
|
||||
-a takes username:password,
|
||||
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:...
|
||||
where "perm" is "accesslevels,username1,username2,..."
|
||||
where "perm" is "permissions,username1,username2,..."
|
||||
and "volflag" is config flags to set on this volume
|
||||
|
||||
list of accesslevels:
|
||||
list of permissions:
|
||||
"r" (read): list folder contents, download files
|
||||
"w" (write): upload files; need "r" to see the uploads
|
||||
"m" (move): move files and folders; need "w" at destination
|
||||
"d" (delete): permanently delete files and folders
|
||||
"g" (get): download files, but cannot see folder contents
|
||||
|
||||
too many volflags to list here, see the other sections
|
||||
|
||||
@@ -279,6 +285,10 @@ def run_argparse(argv, formatter):
|
||||
\033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to
|
||||
generate ".bpm" tags from uploads (f = overwrite tags)
|
||||
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
|
||||
|
||||
\033[0mothers:
|
||||
\033[36mfk=8\033[35m generates per-file accesskeys,
|
||||
which will then be required at the "g" permission
|
||||
\033[0m"""
|
||||
),
|
||||
],
|
||||
@@ -360,6 +370,11 @@ def run_argparse(argv, formatter):
|
||||
ap2 = ap.add_argument_group('safety options')
|
||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
|
||||
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt")
|
||||
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt")
|
||||
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
|
||||
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
|
||||
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
|
||||
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings")
|
||||
|
||||
ap2 = ap.add_argument_group('logging options')
|
||||
ap2.add_argument("-q", action="store_true", help="quiet")
|
||||
@@ -489,7 +504,7 @@ def main(argv=None):
|
||||
if re.match("c[^,]", opt):
|
||||
mod = True
|
||||
na.append("c," + opt[1:])
|
||||
elif re.sub("^[rwmd]*", "", opt) and "," not in opt:
|
||||
elif re.sub("^[rwmdg]*", "", opt) and "," not in opt:
|
||||
mod = True
|
||||
perm = opt[0]
|
||||
if perm == "a":
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (0, 13, 10)
|
||||
CODENAME = "future-proof"
|
||||
BUILD_DT = (2021, 8, 30)
|
||||
VERSION = (1, 0, 5)
|
||||
CODENAME = "sufficient"
|
||||
BUILD_DT = (2021, 9, 19)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -29,17 +29,18 @@ LEELOO_DALLAS = "leeloo_dallas"
|
||||
|
||||
|
||||
class AXS(object):
|
||||
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
||||
def __init__(self, uread=None, uwrite=None, umove=None, udel=None, uget=None):
|
||||
self.uread = {} if uread is None else {k: 1 for k in uread}
|
||||
self.uwrite = {} if uwrite is None else {k: 1 for k in uwrite}
|
||||
self.umove = {} if umove is None else {k: 1 for k in umove}
|
||||
self.udel = {} if udel is None else {k: 1 for k in udel}
|
||||
self.uget = {} if uget is None else {k: 1 for k in uget}
|
||||
|
||||
def __repr__(self):
|
||||
return "AXS({})".format(
|
||||
", ".join(
|
||||
"{}={!r}".format(k, self.__dict__[k])
|
||||
for k in "uread uwrite umove udel".split()
|
||||
for k in "uread uwrite umove udel uget".split()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -215,6 +216,7 @@ class VFS(object):
|
||||
self.awrite = {}
|
||||
self.amove = {}
|
||||
self.adel = {}
|
||||
self.aget = {}
|
||||
else:
|
||||
self.histpath = None
|
||||
self.all_vols = None
|
||||
@@ -222,6 +224,7 @@ class VFS(object):
|
||||
self.awrite = None
|
||||
self.amove = None
|
||||
self.adel = None
|
||||
self.aget = None
|
||||
|
||||
def __repr__(self):
|
||||
return "VFS({})".format(
|
||||
@@ -308,7 +311,7 @@ class VFS(object):
|
||||
|
||||
def can_access(self, vpath, uname):
|
||||
# type: (str, str) -> tuple[bool, bool, bool, bool]
|
||||
"""can Read,Write,Move,Delete"""
|
||||
"""can Read,Write,Move,Delete,Get"""
|
||||
vn, _ = self._find(vpath)
|
||||
c = vn.axs
|
||||
return [
|
||||
@@ -316,10 +319,20 @@ class VFS(object):
|
||||
uname in c.uwrite or "*" in c.uwrite,
|
||||
uname in c.umove or "*" in c.umove,
|
||||
uname in c.udel or "*" in c.udel,
|
||||
uname in c.uget or "*" in c.uget,
|
||||
]
|
||||
|
||||
def get(self, vpath, uname, will_read, will_write, will_move=False, will_del=False):
|
||||
# type: (str, str, bool, bool, bool, bool) -> tuple[VFS, str]
|
||||
def get(
|
||||
self,
|
||||
vpath,
|
||||
uname,
|
||||
will_read,
|
||||
will_write,
|
||||
will_move=False,
|
||||
will_del=False,
|
||||
will_get=False,
|
||||
):
|
||||
# type: (str, str, bool, bool, bool, bool, bool) -> tuple[VFS, str]
|
||||
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
||||
vn, rem = self._find(vpath)
|
||||
c = vn.axs
|
||||
@@ -329,6 +342,7 @@ class VFS(object):
|
||||
[will_write, c.uwrite, "write"],
|
||||
[will_move, c.umove, "move"],
|
||||
[will_del, c.udel, "delete"],
|
||||
[will_get, c.uget, "get"],
|
||||
]:
|
||||
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
|
||||
m = "you don't have {}-access for this location"
|
||||
@@ -368,7 +382,7 @@ class VFS(object):
|
||||
for name, vn2 in sorted(self.nodes.items()):
|
||||
ok = False
|
||||
axs = vn2.axs
|
||||
axs = [axs.uread, axs.uwrite, axs.umove, axs.udel]
|
||||
axs = [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget]
|
||||
for pset in permsets:
|
||||
ok = True
|
||||
for req, lst in zip(pset, axs):
|
||||
@@ -434,7 +448,11 @@ class VFS(object):
|
||||
f2a = os.sep + "dir.txt"
|
||||
f2b = "{0}.hist{0}".format(os.sep)
|
||||
|
||||
g = self.walk("", vrem, [], uname, [[True]], dots, scandir, False)
|
||||
# if multiselect: add all items to archive root
|
||||
# if single folder: the folder itself is the top-level item
|
||||
folder = "" if flt else (vrem.split("/")[-1] or "top")
|
||||
|
||||
g = self.walk(folder, vrem, [], uname, [[True]], dots, scandir, False)
|
||||
for _, _, vpath, apath, files, rd, vd in g:
|
||||
if flt:
|
||||
files = [x for x in files if x[0] in flt]
|
||||
@@ -557,13 +575,21 @@ class AuthSrv(object):
|
||||
|
||||
def _read_vol_str(self, lvl, uname, axs, flags):
|
||||
# type: (str, str, AXS, any) -> None
|
||||
if lvl.strip("crwmd"):
|
||||
if lvl.strip("crwmdg"):
|
||||
raise Exception("invalid volume flag: {},{}".format(lvl, uname))
|
||||
|
||||
if lvl == "c":
|
||||
cval = True
|
||||
if "=" in uname:
|
||||
try:
|
||||
# volume flag with arguments, possibly with a preceding list of bools
|
||||
uname, cval = uname.split("=", 1)
|
||||
except:
|
||||
# just one or more bools
|
||||
cval = True
|
||||
|
||||
while "," in uname:
|
||||
# one or more bools before the final flag; eat them
|
||||
n1, uname = uname.split(",", 1)
|
||||
self._read_volflag(flags, n1, True, False)
|
||||
|
||||
self._read_volflag(flags, uname, cval, False)
|
||||
return
|
||||
@@ -584,6 +610,9 @@ class AuthSrv(object):
|
||||
if "d" in lvl:
|
||||
axs.udel[un] = 1
|
||||
|
||||
if "g" in lvl:
|
||||
axs.uget[un] = 1
|
||||
|
||||
def _read_volflag(self, flags, name, value, is_list):
|
||||
if name not in ["mtp"]:
|
||||
flags[name] = value
|
||||
@@ -621,7 +650,7 @@ class AuthSrv(object):
|
||||
|
||||
if self.args.v:
|
||||
# list of src:dst:permset:permset:...
|
||||
# permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
|
||||
# permset is <rwmdg>[,username][,username] or <c>,<flag>[=args]
|
||||
for v_str in self.args.v:
|
||||
m = re_vol.match(v_str)
|
||||
if not m:
|
||||
@@ -688,20 +717,21 @@ class AuthSrv(object):
|
||||
vfs.all_vols = {}
|
||||
vfs.get_all_vols(vfs.all_vols)
|
||||
|
||||
for perm in "read write move del".split():
|
||||
for perm in "read write move del get".split():
|
||||
axs_key = "u" + perm
|
||||
unames = ["*"] + list(acct.keys())
|
||||
umap = {x: [] for x in unames}
|
||||
for usr in unames:
|
||||
for mp, vol in vfs.all_vols.items():
|
||||
if usr in getattr(vol.axs, axs_key):
|
||||
axs = getattr(vol.axs, axs_key)
|
||||
if usr in axs or "*" in axs:
|
||||
umap[usr].append(mp)
|
||||
setattr(vfs, "a" + perm, umap)
|
||||
|
||||
all_users = {}
|
||||
missing_users = {}
|
||||
for axs in daxs.values():
|
||||
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel]:
|
||||
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget]:
|
||||
for usr in d.keys():
|
||||
all_users[usr] = 1
|
||||
if usr != "*" and usr not in acct:
|
||||
@@ -812,6 +842,11 @@ class AuthSrv(object):
|
||||
if use:
|
||||
vol.lim = lim
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
fk = vol.flags.get("fk")
|
||||
if fk:
|
||||
vol.flags["fk"] = int(fk) if fk is not True else 8
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
|
||||
vol.flags["gz"] = False # def.pk
|
||||
@@ -926,6 +961,7 @@ class AuthSrv(object):
|
||||
[" write", "uwrite"],
|
||||
[" move", "umove"],
|
||||
["delete", "udel"],
|
||||
[" get", "uget"],
|
||||
]:
|
||||
u = list(sorted(getattr(v.axs, attr).keys()))
|
||||
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
|
||||
@@ -993,10 +1029,10 @@ class AuthSrv(object):
|
||||
raise Exception("volume not found: " + v)
|
||||
|
||||
self.log({"users": users, "vols": vols, "flags": flags})
|
||||
m = "/{}: read({}) write({}) move({}) del({})"
|
||||
m = "/{}: read({}) write({}) move({}) del({}) get({})"
|
||||
for k, v in self.vfs.all_vols.items():
|
||||
vc = v.axs
|
||||
self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel))
|
||||
self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.uget))
|
||||
|
||||
flag_v = "v" in flags
|
||||
flag_ln = "ln" in flags
|
||||
@@ -1010,7 +1046,7 @@ class AuthSrv(object):
|
||||
for u in users:
|
||||
self.log("checking /{} as {}".format(v, u))
|
||||
try:
|
||||
vn, _ = self.vfs.get(v, u, True, False, False, False)
|
||||
vn, _ = self.vfs.get(v, u, True, False, False, False, False)
|
||||
except:
|
||||
continue
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ import gzip
|
||||
import time
|
||||
import copy
|
||||
import json
|
||||
import base64
|
||||
import string
|
||||
import socket
|
||||
import ctypes
|
||||
from datetime import datetime
|
||||
from operator import itemgetter
|
||||
import calendar
|
||||
|
||||
try:
|
||||
@@ -21,13 +23,12 @@ except:
|
||||
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
||||
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
||||
from .bos import bos
|
||||
from .authsrv import AuthSrv, Lim
|
||||
from .authsrv import AuthSrv
|
||||
from .szip import StreamZip
|
||||
from .star import StreamTar
|
||||
|
||||
|
||||
NO_CACHE = {"Cache-Control": "no-cache"}
|
||||
NO_STORE = {"Cache-Control": "no-store; max-age=0"}
|
||||
|
||||
|
||||
class HttpCli(object):
|
||||
@@ -53,7 +54,10 @@ class HttpCli(object):
|
||||
self.bufsz = 1024 * 32
|
||||
self.hint = None
|
||||
self.absolute_urls = False
|
||||
self.out_headers = {"Access-Control-Allow-Origin": "*"}
|
||||
self.out_headers = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "no-store; max-age=0",
|
||||
}
|
||||
|
||||
def log(self, msg, c=0):
|
||||
ptn = self.asrv.re_pwd
|
||||
@@ -192,12 +196,27 @@ class HttpCli(object):
|
||||
self.cookies = cookies
|
||||
self.vpath = unquotep(vpath) # not query, so + means +
|
||||
|
||||
pwd = uparam.get("pw")
|
||||
pwd = None
|
||||
ba = self.headers.get("authorization")
|
||||
if ba:
|
||||
try:
|
||||
ba = ba.split(" ")[1].encode("ascii")
|
||||
ba = base64.b64decode(ba).decode("utf-8")
|
||||
# try "pwd", "x:pwd", "pwd:x"
|
||||
for ba in [ba] + ba.split(":", 1)[::-1]:
|
||||
if self.asrv.iacct.get(ba):
|
||||
pwd = ba
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
pwd = uparam.get("pw") or pwd
|
||||
self.uname = self.asrv.iacct.get(pwd, "*")
|
||||
self.rvol = self.asrv.vfs.aread[self.uname]
|
||||
self.wvol = self.asrv.vfs.awrite[self.uname]
|
||||
self.mvol = self.asrv.vfs.amove[self.uname]
|
||||
self.dvol = self.asrv.vfs.adel[self.uname]
|
||||
self.gvol = self.asrv.vfs.aget[self.uname]
|
||||
|
||||
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"):
|
||||
self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0]
|
||||
@@ -212,6 +231,9 @@ class HttpCli(object):
|
||||
|
||||
self.do_log = not self.conn.lf_url or not self.conn.lf_url.search(self.req)
|
||||
|
||||
x = self.asrv.vfs.can_access(self.vpath, self.uname)
|
||||
self.can_read, self.can_write, self.can_move, self.can_delete, self.can_get = x
|
||||
|
||||
try:
|
||||
if self.mode in ["GET", "HEAD"]:
|
||||
return self.handle_get() and self.keepalive
|
||||
@@ -336,8 +358,7 @@ class HttpCli(object):
|
||||
).encode("utf-8", "replace")
|
||||
|
||||
if use302:
|
||||
h = {"Location": "/" + vpath, "Cache-Control": "no-cache"}
|
||||
self.reply(html, status=302, headers=h)
|
||||
self.reply(html, status=302, headers={"Location": "/" + vpath})
|
||||
else:
|
||||
self.reply(html, status=status)
|
||||
|
||||
@@ -363,12 +384,10 @@ class HttpCli(object):
|
||||
static_path = os.path.join(E.mod, "web/", self.vpath[5:])
|
||||
return self.tx_file(static_path)
|
||||
|
||||
x = self.asrv.vfs.can_access(self.vpath, self.uname)
|
||||
self.can_read, self.can_write, self.can_move, self.can_delete = x
|
||||
if not self.can_read and not self.can_write:
|
||||
if not self.can_read and not self.can_write and not self.can_get:
|
||||
if self.vpath:
|
||||
self.log("inaccessible: [{}]".format(self.vpath))
|
||||
raise Pebkac(404)
|
||||
return self.tx_404()
|
||||
|
||||
self.uparam["h"] = False
|
||||
|
||||
@@ -490,7 +509,7 @@ class HttpCli(object):
|
||||
if "get" in opt:
|
||||
return self.handle_get()
|
||||
|
||||
raise Pebkac(405, "POST({}) is disabled".format(ctype))
|
||||
raise Pebkac(405, "POST({}) is disabled in server config".format(ctype))
|
||||
|
||||
raise Pebkac(405, "don't know how to handle POST({})".format(ctype))
|
||||
|
||||
@@ -872,8 +891,12 @@ class HttpCli(object):
|
||||
pwd = self.parser.require("cppwd", 64)
|
||||
self.parser.drop()
|
||||
|
||||
dst = "/?h"
|
||||
if self.vpath:
|
||||
dst = "/" + quotep(self.vpath)
|
||||
|
||||
ck, msg = self.get_pwd_cookie(pwd)
|
||||
html = self.j2("msg", h1=msg, h2='<a href="/?h">ack</a>', redir="/?h")
|
||||
html = self.j2("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst)
|
||||
self.reply(html.encode("utf-8"), headers={"Set-Cookie": ck})
|
||||
return True
|
||||
|
||||
@@ -1009,7 +1032,7 @@ class HttpCli(object):
|
||||
bos.unlink(abspath)
|
||||
raise
|
||||
|
||||
files.append([sz, sha512_hex, p_file, fname])
|
||||
files.append([sz, sha512_hex, p_file, fname, abspath])
|
||||
dbv, vrem = vfs.get_dbv(rem)
|
||||
self.conn.hsrv.broker.put(
|
||||
False,
|
||||
@@ -1061,10 +1084,19 @@ class HttpCli(object):
|
||||
jmsg["error"] = errmsg
|
||||
errmsg = "ERROR: " + errmsg
|
||||
|
||||
for sz, sha512, ofn, lfn in files:
|
||||
for sz, sha512, ofn, lfn, ap in files:
|
||||
vsuf = ""
|
||||
if self.can_read and "fk" in vfs.flags:
|
||||
vsuf = "?k=" + gen_filekey(
|
||||
self.args.fk_salt,
|
||||
abspath,
|
||||
sz,
|
||||
0 if ANYWIN or not ap else bos.stat(ap).st_ino,
|
||||
)[: vfs.flags["fk"]]
|
||||
|
||||
vpath = "{}/{}".format(upload_vpath, lfn).strip("/")
|
||||
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
|
||||
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
|
||||
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a> {}\n'.format(
|
||||
sha512[:56], sz, quotep(vpath) + vsuf, html_escape(ofn, crlf=True), vsuf
|
||||
)
|
||||
# truncated SHA-512 prevents length extension attacks;
|
||||
# using SHA-512/224, optionally SHA-512/256 = :64
|
||||
@@ -1072,13 +1104,13 @@ class HttpCli(object):
|
||||
"url": "{}://{}/{}".format(
|
||||
"https" if self.tls else "http",
|
||||
self.headers.get("host", "copyparty"),
|
||||
vpath,
|
||||
vpath + vsuf,
|
||||
),
|
||||
"sha512": sha512[:56],
|
||||
"sz": sz,
|
||||
"fn": lfn,
|
||||
"fn_orig": ofn,
|
||||
"path": vpath,
|
||||
"path": vpath + vsuf,
|
||||
}
|
||||
jmsg["files"].append(jpart)
|
||||
|
||||
@@ -1262,7 +1294,7 @@ class HttpCli(object):
|
||||
break
|
||||
|
||||
if not editions:
|
||||
raise Pebkac(404)
|
||||
return self.tx_404()
|
||||
|
||||
#
|
||||
# if-modified
|
||||
@@ -1373,7 +1405,11 @@ class HttpCli(object):
|
||||
#
|
||||
# send reply
|
||||
|
||||
if not is_compressed and "cache" not in self.uparam:
|
||||
if is_compressed:
|
||||
self.out_headers["Cache-Control"] = "max-age=573"
|
||||
elif "cache" in self.uparam:
|
||||
self.out_headers["Cache-Control"] = "max-age=69"
|
||||
else:
|
||||
self.out_headers.update(NO_CACHE)
|
||||
|
||||
self.out_headers["Accept-Ranges"] = "bytes"
|
||||
@@ -1500,6 +1536,10 @@ class HttpCli(object):
|
||||
def tx_md(self, fs_path):
|
||||
logmsg = "{:4} {} ".format("", self.req)
|
||||
|
||||
if not self.can_write:
|
||||
if "edit" in self.uparam or "edit2" in self.uparam:
|
||||
return self.tx_404()
|
||||
|
||||
tpl = "mde" if "edit2" in self.uparam else "md"
|
||||
html_path = os.path.join(E.mod, "web", "{}.html".format(tpl))
|
||||
template = self.j2(tpl)
|
||||
@@ -1522,6 +1562,10 @@ class HttpCli(object):
|
||||
self.out_headers.update(NO_CACHE)
|
||||
status = 200 if do_send else 304
|
||||
|
||||
arg_base = "?"
|
||||
if "k" in self.uparam:
|
||||
arg_base = "?k={}&".format(self.uparam["k"])
|
||||
|
||||
boundary = "\roll\tide"
|
||||
targs = {
|
||||
"edit": "edit" in self.uparam,
|
||||
@@ -1531,6 +1575,7 @@ class HttpCli(object):
|
||||
"md_chk_rate": self.args.mcr,
|
||||
"md": boundary,
|
||||
"ts": self.conn.hsrv.cachebuster(),
|
||||
"arg_base": arg_base,
|
||||
}
|
||||
html = template.render(**targs).encode("utf-8", "replace")
|
||||
html = html.split(boundary.encode("utf-8"))
|
||||
@@ -1581,6 +1626,7 @@ class HttpCli(object):
|
||||
html = self.j2(
|
||||
"splash",
|
||||
this=self,
|
||||
qvpath=quotep(self.vpath),
|
||||
rvol=rvol,
|
||||
wvol=wvol,
|
||||
avol=avol,
|
||||
@@ -1591,7 +1637,13 @@ class HttpCli(object):
|
||||
mtpq=vs["mtpq"],
|
||||
url_suf=suf,
|
||||
)
|
||||
self.reply(html.encode("utf-8"), headers=NO_STORE)
|
||||
self.reply(html.encode("utf-8"))
|
||||
return True
|
||||
|
||||
def tx_404(self):
|
||||
m = '<h1>404 not found ┐( ´ -`)┌</h1><p>or maybe you don\'t have access -- try logging in or <a href="/?h">go home</a></p>'
|
||||
html = self.j2("splash", this=self, qvpath=quotep(self.vpath), msg=m)
|
||||
self.reply(html.encode("utf-8"), status=404)
|
||||
return True
|
||||
|
||||
def scanvol(self):
|
||||
@@ -1599,7 +1651,7 @@ class HttpCli(object):
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_rescan:
|
||||
raise Pebkac(403, "disabled by argv")
|
||||
raise Pebkac(403, "the rescan feature is disabled in server config")
|
||||
|
||||
vn, _ = self.asrv.vfs.get(self.vpath, self.uname, True, True)
|
||||
|
||||
@@ -1618,7 +1670,7 @@ class HttpCli(object):
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_stack:
|
||||
raise Pebkac(403, "disabled by argv")
|
||||
raise Pebkac(403, "the stackdump feature is disabled in server config")
|
||||
|
||||
ret = "<pre>{}\n{}".format(time.time(), alltrace())
|
||||
self.reply(ret.encode("utf-8"))
|
||||
@@ -1682,7 +1734,7 @@ class HttpCli(object):
|
||||
|
||||
def tx_ups(self):
|
||||
if not self.args.unpost:
|
||||
raise Pebkac(400, "the unpost feature was disabled by server config")
|
||||
raise Pebkac(400, "the unpost feature is disabled in server config")
|
||||
|
||||
filt = self.uparam.get("filter")
|
||||
lm = "ups [{}]".format(filt)
|
||||
@@ -1720,7 +1772,7 @@ class HttpCli(object):
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_del:
|
||||
raise Pebkac(403, "disabled by argv")
|
||||
raise Pebkac(403, "the delete feature is disabled in server config")
|
||||
|
||||
if not req:
|
||||
req = [self.vpath]
|
||||
@@ -1733,7 +1785,7 @@ class HttpCli(object):
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_mv:
|
||||
raise Pebkac(403, "disabled by argv")
|
||||
raise Pebkac(403, "the rename/move feature is disabled in server config")
|
||||
|
||||
# full path of new loc (incl filename)
|
||||
dst = self.uparam.get("move")
|
||||
@@ -1767,15 +1819,15 @@ class HttpCli(object):
|
||||
try:
|
||||
st = bos.stat(abspath)
|
||||
except:
|
||||
raise Pebkac(404)
|
||||
return self.tx_404()
|
||||
|
||||
if rem.startswith(".hist/up2k.") or (
|
||||
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
||||
):
|
||||
raise Pebkac(403)
|
||||
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
if self.can_read:
|
||||
if rem.startswith(".hist/up2k.") or (
|
||||
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
||||
):
|
||||
raise Pebkac(403)
|
||||
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
th_fmt = self.uparam.get("th")
|
||||
if th_fmt is not None:
|
||||
if is_dir:
|
||||
@@ -1800,11 +1852,23 @@ class HttpCli(object):
|
||||
|
||||
return self.tx_ico(rem)
|
||||
|
||||
if not is_dir:
|
||||
if abspath.endswith(".md") and "raw" not in self.uparam:
|
||||
return self.tx_md(abspath)
|
||||
if not is_dir and (self.can_read or self.can_get):
|
||||
if not self.can_read and "fk" in vn.flags:
|
||||
correct = gen_filekey(
|
||||
self.args.fk_salt, abspath, st.st_size, 0 if ANYWIN else st.st_ino
|
||||
)[: vn.flags["fk"]]
|
||||
got = self.uparam.get("k")
|
||||
if got != correct:
|
||||
self.log("wrong filekey, want {}, got {}".format(correct, got))
|
||||
return self.tx_404()
|
||||
|
||||
return self.tx_file(abspath)
|
||||
if abspath.endswith(".md") and "raw" not in self.uparam:
|
||||
return self.tx_md(abspath)
|
||||
|
||||
return self.tx_file(abspath)
|
||||
|
||||
elif is_dir and not self.can_read and not self.can_write:
|
||||
return self.tx_404()
|
||||
|
||||
srv_info = []
|
||||
|
||||
@@ -1844,6 +1908,8 @@ class HttpCli(object):
|
||||
perms.append("move")
|
||||
if self.can_delete:
|
||||
perms.append("delete")
|
||||
if self.can_get:
|
||||
perms.append("get")
|
||||
|
||||
url_suf = self.urlq({}, [])
|
||||
is_ls = "ls" in self.uparam
|
||||
@@ -1853,11 +1919,21 @@ class HttpCli(object):
|
||||
tpl = "browser2"
|
||||
|
||||
logues = ["", ""]
|
||||
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
|
||||
fn = os.path.join(abspath, fn)
|
||||
if bos.path.exists(fn):
|
||||
with open(fsenc(fn), "rb") as f:
|
||||
logues[n] = f.read().decode("utf-8")
|
||||
if not self.args.no_logues:
|
||||
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
|
||||
fn = os.path.join(abspath, fn)
|
||||
if bos.path.exists(fn):
|
||||
with open(fsenc(fn), "rb") as f:
|
||||
logues[n] = f.read().decode("utf-8")
|
||||
|
||||
readme = ""
|
||||
if not self.args.no_readme and not logues[1]:
|
||||
for fn in ["README.md", "readme.md"]:
|
||||
fn = os.path.join(abspath, fn)
|
||||
if bos.path.exists(fn):
|
||||
with open(fsenc(fn), "rb") as f:
|
||||
readme = f.read().decode("utf-8")
|
||||
break
|
||||
|
||||
ls_ret = {
|
||||
"dirs": [],
|
||||
@@ -1867,6 +1943,7 @@ class HttpCli(object):
|
||||
"acct": self.uname,
|
||||
"perms": perms,
|
||||
"logues": logues,
|
||||
"readme": readme,
|
||||
}
|
||||
j2a = {
|
||||
"vdir": quotep(self.vpath),
|
||||
@@ -1885,24 +1962,24 @@ class HttpCli(object):
|
||||
"have_b_u": (self.can_write and self.uparam.get("b") == "u"),
|
||||
"url_suf": url_suf,
|
||||
"logues": logues,
|
||||
"readme": readme,
|
||||
"title": html_escape(self.vpath, crlf=True),
|
||||
"srv_info": srv_info,
|
||||
}
|
||||
if not self.can_read:
|
||||
if is_ls:
|
||||
ret = json.dumps(ls_ret)
|
||||
self.reply(
|
||||
ret.encode("utf-8", "replace"),
|
||||
mime="application/json",
|
||||
headers=NO_STORE,
|
||||
)
|
||||
self.reply(ret.encode("utf-8", "replace"), mime="application/json")
|
||||
return True
|
||||
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
raise Pebkac(404)
|
||||
return self.tx_404()
|
||||
|
||||
if "zip" in self.uparam or "tar" in self.uparam:
|
||||
raise Pebkac(403)
|
||||
|
||||
html = self.j2(tpl, **j2a)
|
||||
self.reply(html.encode("utf-8", "replace"), headers=NO_STORE)
|
||||
self.reply(html.encode("utf-8", "replace"))
|
||||
return True
|
||||
|
||||
for k in ["zip", "tar"]:
|
||||
@@ -1946,6 +2023,8 @@ class HttpCli(object):
|
||||
idx = self.conn.get_u2idx()
|
||||
icur = idx.get_cur(dbv.realpath)
|
||||
|
||||
add_fk = vn.flags.get("fk")
|
||||
|
||||
dirs = []
|
||||
files = []
|
||||
for fn in vfs_ls:
|
||||
@@ -1991,9 +2070,19 @@ class HttpCli(object):
|
||||
except:
|
||||
ext = "%"
|
||||
|
||||
if add_fk:
|
||||
href = "{}?k={}".format(
|
||||
quotep(href),
|
||||
gen_filekey(
|
||||
self.args.fk_salt, fspath, sz, 0 if ANYWIN else inf.st_ino
|
||||
)[:add_fk],
|
||||
)
|
||||
else:
|
||||
href = quotep(href)
|
||||
|
||||
item = {
|
||||
"lead": margin,
|
||||
"href": quotep(href),
|
||||
"href": href,
|
||||
"name": fn,
|
||||
"sz": sz,
|
||||
"ext": ext,
|
||||
@@ -2061,13 +2150,14 @@ class HttpCli(object):
|
||||
ls_ret["files"] = files
|
||||
ls_ret["taglist"] = taglist
|
||||
ret = json.dumps(ls_ret)
|
||||
self.reply(
|
||||
ret.encode("utf-8", "replace"),
|
||||
mime="application/json",
|
||||
headers=NO_STORE,
|
||||
)
|
||||
self.reply(ret.encode("utf-8", "replace"), mime="application/json")
|
||||
return True
|
||||
|
||||
for d in dirs:
|
||||
d["name"] += "/"
|
||||
|
||||
dirs.sort(key=itemgetter("name"))
|
||||
|
||||
j2a["files"] = dirs + files
|
||||
j2a["logues"] = logues
|
||||
j2a["taglist"] = taglist
|
||||
@@ -2079,5 +2169,5 @@ class HttpCli(object):
|
||||
j2a["css"] = self.args.css_browser
|
||||
|
||||
html = self.j2(tpl, **j2a)
|
||||
self.reply(html.encode("utf-8", "replace"), headers=NO_STORE)
|
||||
self.reply(html.encode("utf-8", "replace"))
|
||||
return True
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
This is Victor Stinner's pure-Python implementation of PEP 383: the "surrogateescape" error
|
||||
handler of Python 3.
|
||||
@@ -171,7 +173,7 @@ FS_ENCODING = sys.getfilesystemencoding()
|
||||
|
||||
if WINDOWS and not PY3:
|
||||
# py2 thinks win* is mbcs, probably a bug? anyways this works
|
||||
FS_ENCODING = 'utf-8'
|
||||
FS_ENCODING = "utf-8"
|
||||
|
||||
|
||||
# normalize the filesystem encoding name.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
@@ -205,6 +204,8 @@ class SvcHub(object):
|
||||
if self.stopping:
|
||||
return
|
||||
|
||||
# start_log_thrs(print, 0.1, 1)
|
||||
|
||||
self.stopping = True
|
||||
self.stop_req = True
|
||||
with self.stop_cond:
|
||||
|
||||
@@ -177,7 +177,7 @@ class TcpSrv(object):
|
||||
eps = self.ips_linux()
|
||||
|
||||
if "0.0.0.0" not in listen_ips:
|
||||
eps = {k: v for k, v in eps if k in listen_ips}
|
||||
eps = {k: v for k, v in eps.items() if k in listen_ips}
|
||||
|
||||
default_route = None
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
@@ -270,13 +270,14 @@ class ThumbSrv(object):
|
||||
fmts += ["RGBA", "LA"]
|
||||
args["method"] = 6
|
||||
else:
|
||||
pass # default q = 75
|
||||
# default q = 75
|
||||
args["progressive"] = True
|
||||
|
||||
if im.mode not in fmts:
|
||||
# print("conv {}".format(im.mode))
|
||||
im = im.convert("RGB")
|
||||
|
||||
im.save(tpath, quality=40, method=6)
|
||||
im.save(tpath, **args)
|
||||
|
||||
def conv_ffmpeg(self, abspath, tpath):
|
||||
ret, _ = ffprobe(abspath)
|
||||
|
||||
@@ -7,8 +7,8 @@ import time
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
from .__init__ import unicode
|
||||
from .util import s3dec, Pebkac, min_ex
|
||||
from .__init__ import ANYWIN, unicode
|
||||
from .util import absreal, s3dec, Pebkac, min_ex, gen_filekey
|
||||
from .bos import bos
|
||||
from .up2k import up2k_wark_from_hashlist
|
||||
|
||||
@@ -242,6 +242,7 @@ class U2idx(object):
|
||||
self.active_cur = cur
|
||||
|
||||
sret = []
|
||||
fk = flags.get("fk")
|
||||
c = cur.execute(q, v)
|
||||
for hit in c:
|
||||
w, ts, sz, rd, fn, ip, at = hit
|
||||
@@ -252,6 +253,20 @@ class U2idx(object):
|
||||
if rd.startswith("//") or fn.startswith("//"):
|
||||
rd, fn = s3dec(rd, fn)
|
||||
|
||||
if fk:
|
||||
try:
|
||||
ap = absreal(os.path.join(ptop, rd, fn))
|
||||
inf = bos.stat(ap)
|
||||
except:
|
||||
continue
|
||||
|
||||
fn += (
|
||||
"?k="
|
||||
+ gen_filekey(
|
||||
self.args.fk_salt, ap, sz, 0 if ANYWIN else inf.st_ino
|
||||
)[:fk]
|
||||
)
|
||||
|
||||
rp = "/".join([x for x in [vtop, rd, fn] if x])
|
||||
sret.append({"ts": int(ts), "sz": sz, "rp": rp, "w": w[:16]})
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ class Up2k(object):
|
||||
if vp:
|
||||
fvp = "{}/{}".format(vp, fvp)
|
||||
|
||||
self._handle_rm(LEELOO_DALLAS, None, fvp)
|
||||
self._handle_rm(LEELOO_DALLAS, None, fvp, True)
|
||||
nrm += 1
|
||||
|
||||
if nrm:
|
||||
@@ -838,6 +838,7 @@ class Up2k(object):
|
||||
|
||||
cur.connection.commit()
|
||||
if n_done:
|
||||
self.log("mtp: scanned {} files in {}".format(n_done, ptop), c=6)
|
||||
cur.execute("vacuum")
|
||||
|
||||
wcur.close()
|
||||
@@ -1384,7 +1385,7 @@ class Up2k(object):
|
||||
ok = {}
|
||||
ng = {}
|
||||
for vp in vpaths:
|
||||
a, b, c = self._handle_rm(uname, ip, vp)
|
||||
a, b, c = self._handle_rm(uname, ip, vp, False)
|
||||
n_files += a
|
||||
for k in b:
|
||||
ok[k] = 1
|
||||
@@ -1397,7 +1398,7 @@ class Up2k(object):
|
||||
|
||||
return "deleted {} files (and {}/{} folders)".format(n_files, ok, ok + ng)
|
||||
|
||||
def _handle_rm(self, uname, ip, vpath):
|
||||
def _handle_rm(self, uname, ip, vpath, rm_topdir):
|
||||
try:
|
||||
permsets = [[True, False, False, True]]
|
||||
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||
@@ -1405,7 +1406,7 @@ class Up2k(object):
|
||||
except:
|
||||
# unpost with missing permissions? try read+write and verify with db
|
||||
if not self.args.unpost:
|
||||
raise Pebkac(400, "the unpost feature was disabled by server config")
|
||||
raise Pebkac(400, "the unpost feature is disabled in server config")
|
||||
|
||||
unpost = True
|
||||
permsets = [[True, True]]
|
||||
@@ -1455,16 +1456,18 @@ class Up2k(object):
|
||||
self.log("rm {}\n {}".format(vpath, abspath))
|
||||
_ = dbv.get(volpath, uname, *permsets[0])
|
||||
with self.mutex:
|
||||
cur = None
|
||||
try:
|
||||
ptop = dbv.realpath
|
||||
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||
self._forget_file(ptop, volpath, cur, wark, True)
|
||||
finally:
|
||||
cur.connection.commit()
|
||||
if cur:
|
||||
cur.connection.commit()
|
||||
|
||||
bos.unlink(abspath)
|
||||
|
||||
rm = rmdirs(self.log_func, scandir, True, atop)
|
||||
rm = rmdirs(self.log_func, scandir, True, atop, 1 if rm_topdir else 0)
|
||||
return n_files, rm[0], rm[1]
|
||||
|
||||
def handle_mv(self, uname, svp, dvp):
|
||||
@@ -1506,7 +1509,7 @@ class Up2k(object):
|
||||
with self.mutex:
|
||||
self._mv_file(uname, svpf, dvpf)
|
||||
|
||||
rmdirs(self.log_func, scandir, True, sabs)
|
||||
rmdirs(self.log_func, scandir, True, sabs, 1)
|
||||
return "k"
|
||||
|
||||
def _mv_file(self, uname, svp, dvp):
|
||||
@@ -1520,6 +1523,14 @@ class Up2k(object):
|
||||
dabs = dvn.canonical(drem)
|
||||
drd, dfn = vsplit(drem)
|
||||
|
||||
n1 = svp.split("/")[-1]
|
||||
n2 = dvp.split("/")[-1]
|
||||
if n1.startswith(".") or n2.startswith("."):
|
||||
if self.args.no_dot_mv:
|
||||
raise Pebkac(400, "moving dotfiles is disabled in server config")
|
||||
elif self.args.no_dot_ren and n1 != n2:
|
||||
raise Pebkac(400, "renaming dotfiles is disabled in server config")
|
||||
|
||||
if bos.path.exists(dabs):
|
||||
raise Pebkac(400, "mv2: target file exists")
|
||||
|
||||
@@ -1575,7 +1586,7 @@ class Up2k(object):
|
||||
def _find_from_vpath(self, ptop, vrem):
|
||||
cur = self.cur.get(ptop)
|
||||
if not cur:
|
||||
return None, None
|
||||
return [None] * 6
|
||||
|
||||
rd, fn = vsplit(vrem)
|
||||
q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1"
|
||||
|
||||
@@ -19,7 +19,7 @@ import subprocess as sp # nosec
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
|
||||
from .__init__ import PY2, WINDOWS, ANYWIN, VT100
|
||||
from .__init__ import PY2, WINDOWS, ANYWIN, VT100, unicode
|
||||
from .stolen import surrogateescape
|
||||
|
||||
FAKE_MP = False
|
||||
@@ -169,7 +169,7 @@ class Cooldown(object):
|
||||
return ret
|
||||
|
||||
|
||||
class Unrecv(object):
|
||||
class _Unrecv(object):
|
||||
"""
|
||||
undo any number of socket recv ops
|
||||
"""
|
||||
@@ -189,10 +189,68 @@ class Unrecv(object):
|
||||
except:
|
||||
return b""
|
||||
|
||||
def recv_ex(self, nbytes):
|
||||
"""read an exact number of bytes"""
|
||||
ret = self.recv(nbytes)
|
||||
while ret and len(ret) < nbytes:
|
||||
buf = self.recv(nbytes - len(ret))
|
||||
if not buf:
|
||||
break
|
||||
|
||||
ret += buf
|
||||
|
||||
return ret
|
||||
|
||||
def unrecv(self, buf):
|
||||
self.buf = buf + self.buf
|
||||
|
||||
|
||||
class _LUnrecv(object):
|
||||
"""
|
||||
with expensive debug logging
|
||||
"""
|
||||
|
||||
def __init__(self, s):
|
||||
self.s = s
|
||||
self.buf = b""
|
||||
|
||||
def recv(self, nbytes):
|
||||
if self.buf:
|
||||
ret = self.buf[:nbytes]
|
||||
self.buf = self.buf[nbytes:]
|
||||
m = "\033[0;7mur:pop:\033[0;1;32m {}\n\033[0;7mur:rem:\033[0;1;35m {}\033[0m\n"
|
||||
print(m.format(ret, self.buf), end="")
|
||||
return ret
|
||||
|
||||
try:
|
||||
ret = self.s.recv(nbytes)
|
||||
m = "\033[0;7mur:recv\033[0;1;33m {}\033[0m\n"
|
||||
print(m.format(ret), end="")
|
||||
return ret
|
||||
except:
|
||||
return b""
|
||||
|
||||
def recv_ex(self, nbytes):
|
||||
"""read an exact number of bytes"""
|
||||
ret = self.recv(nbytes)
|
||||
while ret and len(ret) < nbytes:
|
||||
buf = self.recv(nbytes - len(ret))
|
||||
if not buf:
|
||||
break
|
||||
|
||||
ret += buf
|
||||
|
||||
return ret
|
||||
|
||||
def unrecv(self, buf):
|
||||
self.buf = buf + self.buf
|
||||
m = "\033[0;7mur:push\033[0;1;31m {}\n\033[0;7mur:rem:\033[0;1;35m {}\033[0m\n"
|
||||
print(m.format(buf, self.buf), end="")
|
||||
|
||||
|
||||
Unrecv = _Unrecv
|
||||
|
||||
|
||||
class ProgressPrinter(threading.Thread):
|
||||
"""
|
||||
periodically print progress info without linefeeds
|
||||
@@ -317,7 +375,7 @@ def stackmon(fp, ival, suffix):
|
||||
|
||||
|
||||
def start_log_thrs(logger, ival, nid):
|
||||
ival = int(ival)
|
||||
ival = float(ival)
|
||||
tname = lname = "log-thrs"
|
||||
if nid:
|
||||
tname = "logthr-n{}-i{:x}".format(nid, os.getpid())
|
||||
@@ -587,19 +645,21 @@ class MultipartParser(object):
|
||||
yields [fieldname, unsanitized_filename, fieldvalue]
|
||||
where fieldvalue yields chunks of data
|
||||
"""
|
||||
while True:
|
||||
run = True
|
||||
while run:
|
||||
fieldname, filename = self._read_header()
|
||||
yield [fieldname, filename, self._read_data()]
|
||||
|
||||
tail = self.sr.recv(2)
|
||||
tail = self.sr.recv_ex(2)
|
||||
|
||||
if tail == b"--":
|
||||
# EOF indicated by this immediately after final boundary
|
||||
self.sr.recv(2)
|
||||
return
|
||||
tail = self.sr.recv_ex(2)
|
||||
run = False
|
||||
|
||||
if tail != b"\r\n":
|
||||
raise Pebkac(400, "protocol error after field value")
|
||||
m = "protocol error after field value: want b'\\r\\n', got {!r}"
|
||||
raise Pebkac(400, m.format(tail))
|
||||
|
||||
def _read_value(self, iterator, max_len):
|
||||
ret = b""
|
||||
@@ -685,6 +745,14 @@ def read_header(sr):
|
||||
return ret[:ofs].decode("utf-8", "surrogateescape").lstrip("\r\n").split("\r\n")
|
||||
|
||||
|
||||
def gen_filekey(salt, fspath, fsize, inode):
|
||||
return base64.urlsafe_b64encode(
|
||||
hashlib.sha512(
|
||||
"{} {} {} {}".format(salt, fspath, fsize, inode).encode("utf-8", "replace")
|
||||
).digest()
|
||||
).decode("ascii")
|
||||
|
||||
|
||||
def humansize(sz, terse=False):
|
||||
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
|
||||
if sz < 1024:
|
||||
@@ -985,8 +1053,12 @@ def read_socket_chunked(sr, log=None):
|
||||
raise Pebkac(400, err)
|
||||
|
||||
if chunklen == 0:
|
||||
sr.recv(2) # \r\n after final chunk
|
||||
return
|
||||
x = sr.recv_ex(2)
|
||||
if x == b"\r\n":
|
||||
return
|
||||
|
||||
m = "protocol error after final chunk: want b'\\r\\n', got {!r}"
|
||||
raise Pebkac(400, m.format(x))
|
||||
|
||||
if log:
|
||||
log("receiving {} byte chunk".format(chunklen))
|
||||
@@ -994,7 +1066,10 @@ def read_socket_chunked(sr, log=None):
|
||||
for chunk in read_socket(sr, chunklen):
|
||||
yield chunk
|
||||
|
||||
sr.recv(2) # \r\n after each chunk too
|
||||
x = sr.recv_ex(2)
|
||||
if x != b"\r\n":
|
||||
m = "protocol error in chunk separator: want b'\\r\\n', got {!r}"
|
||||
raise Pebkac(400, m.format(x))
|
||||
|
||||
|
||||
def yieldfile(fn):
|
||||
@@ -1089,7 +1164,7 @@ def statdir(logger, scandir, lstat, top):
|
||||
logger(src, "{} @ {}".format(repr(ex), top), 1)
|
||||
|
||||
|
||||
def rmdirs(logger, scandir, lstat, top):
|
||||
def rmdirs(logger, scandir, lstat, top, depth):
|
||||
if not os.path.exists(fsenc(top)) or not os.path.isdir(fsenc(top)):
|
||||
top = os.path.dirname(top)
|
||||
|
||||
@@ -1099,15 +1174,16 @@ def rmdirs(logger, scandir, lstat, top):
|
||||
ok = []
|
||||
ng = []
|
||||
for d in dirs[::-1]:
|
||||
a, b = rmdirs(logger, scandir, lstat, d)
|
||||
a, b = rmdirs(logger, scandir, lstat, d, depth + 1)
|
||||
ok += a
|
||||
ng += b
|
||||
|
||||
try:
|
||||
os.rmdir(fsenc(top))
|
||||
ok.append(top)
|
||||
except:
|
||||
ng.append(top)
|
||||
if depth:
|
||||
try:
|
||||
os.rmdir(fsenc(top))
|
||||
ok.append(top)
|
||||
except:
|
||||
ng.append(top)
|
||||
|
||||
return ok, ng
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
:root {
|
||||
--grid-sz: 10em;
|
||||
--grid-ln: 3;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'scp';
|
||||
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(/.cpr/deps/scp.woff2) format('woff2');
|
||||
--nav-sz: 16em;
|
||||
}
|
||||
* {
|
||||
line-height: 1.2em;
|
||||
@@ -177,6 +174,13 @@ a, #files tbody div a:last-child {
|
||||
#epi.logue {
|
||||
margin: .8em 0;
|
||||
}
|
||||
.mdo {
|
||||
max-width: 52em;
|
||||
}
|
||||
.mdo,
|
||||
.mdo * {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
#srv_info {
|
||||
color: #a73;
|
||||
background: #333;
|
||||
@@ -343,17 +347,19 @@ html.light #ggrid>a.sel {
|
||||
}
|
||||
#wtgrid,
|
||||
#wtico {
|
||||
cursor: url(/.cpr/dd/4.png), pointer;
|
||||
animation: cursor 500ms;
|
||||
position: relative;
|
||||
font-size: .9em;
|
||||
top: -.04em;
|
||||
}
|
||||
#wtgrid {
|
||||
font-size: .75em;
|
||||
padding: .1em;
|
||||
top: -.12em;
|
||||
}
|
||||
#wtgrid:hover,
|
||||
#wtico {
|
||||
cursor: url(/.cpr/dd/4.png), pointer;
|
||||
animation: cursor 500ms;
|
||||
}
|
||||
#wtico:hover {
|
||||
animation: cursor 500ms infinite;
|
||||
}
|
||||
@@ -381,23 +387,26 @@ html.light #ggrid>a.sel {
|
||||
background: #3c3c3c;
|
||||
box-shadow: 0 0 .5em #222;
|
||||
border-radius: .3em 0 0 0;
|
||||
padding: 0;
|
||||
padding: 0 0 0 .1em;
|
||||
color: #fff;
|
||||
}
|
||||
#wzip, #wnp {
|
||||
#wfm, #wzip, #wnp {
|
||||
display: none;
|
||||
margin-right: .3em;
|
||||
padding-right: .3em;
|
||||
border-right: .1em solid #555;
|
||||
}
|
||||
#wnp a {
|
||||
position: relative;
|
||||
font-size: .47em;
|
||||
margin: 0 .1em;
|
||||
top: -.4em;
|
||||
#wzip, #wnp {
|
||||
margin-right: .2em;
|
||||
padding-right: .2em;
|
||||
border: 1px solid #555;
|
||||
border-width: 0 .1em 0 0;
|
||||
}
|
||||
#wnp a+a {
|
||||
margin-left: .33em;
|
||||
#wfm.act+#wzip,
|
||||
#wfm.act+#wzip+#wnp {
|
||||
margin-left: .2em;
|
||||
padding-left: .2em;
|
||||
border-left-width: .1em;
|
||||
}
|
||||
#wfm.act {
|
||||
display: inline-block;
|
||||
}
|
||||
#wtoggle,
|
||||
#wtoggle * {
|
||||
@@ -411,6 +420,7 @@ html.light #ggrid>a.sel {
|
||||
display: none;
|
||||
}
|
||||
#wfm a,
|
||||
#wnp a,
|
||||
#wzip a {
|
||||
font-size: .5em;
|
||||
padding: 0 .3em;
|
||||
@@ -418,10 +428,14 @@ html.light #ggrid>a.sel {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
#wfm span {
|
||||
#wfm span,
|
||||
#wnp span {
|
||||
font-size: .6em;
|
||||
display: block;
|
||||
}
|
||||
#wnp span {
|
||||
font-size: .7em;
|
||||
}
|
||||
#wfm a:not(.en) {
|
||||
opacity: .3;
|
||||
color: #f6c;
|
||||
@@ -558,7 +572,7 @@ html.light #wfm a:not(.en) {
|
||||
padding: .5em;
|
||||
border-radius: 0 1em 1em 0;
|
||||
border-width: .15em .3em .3em 0;
|
||||
max-width: 40em;
|
||||
max-width: 41em;
|
||||
}
|
||||
.opbox input {
|
||||
margin: .5em;
|
||||
@@ -743,6 +757,9 @@ input.eq_gain {
|
||||
color: #400;
|
||||
text-shadow: none;
|
||||
}
|
||||
.tgl.btn.on:hover {
|
||||
background: #fe8;
|
||||
}
|
||||
#detree {
|
||||
padding: .3em .5em;
|
||||
font-size: 1.5em;
|
||||
@@ -778,6 +795,19 @@ input.eq_gain {
|
||||
width: calc(100% - 2em);
|
||||
line-height: 1em;
|
||||
}
|
||||
#tree.nowrap #treeul li {
|
||||
min-height: 1.4em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#tree.nowrap #treeul a+a:hover {
|
||||
background: rgba(34, 34, 34, 0.67);
|
||||
min-width: calc(var(--nav-sz) - 2em);
|
||||
width: auto;
|
||||
}
|
||||
html.light #tree.nowrap #treeul a+a:hover {
|
||||
background: rgba(255, 255, 255, 0.67);
|
||||
color: #000;
|
||||
}
|
||||
#treeul a+a:hover {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
@@ -806,8 +836,6 @@ input.eq_gain {
|
||||
width: 1em;
|
||||
border-radius: .2em;
|
||||
margin: -1.2em auto 0 auto;
|
||||
top: 2em;
|
||||
position: relative;
|
||||
background: #444;
|
||||
}
|
||||
#files th span {
|
||||
@@ -871,6 +899,9 @@ input.eq_gain {
|
||||
#u2turbo.on+#u2tdate {
|
||||
opacity: 1;
|
||||
}
|
||||
#wraptree.on+#hovertree {
|
||||
display: none;
|
||||
}
|
||||
#ghead {
|
||||
background: #3c3c3c;
|
||||
border: 1px solid #444;
|
||||
@@ -1105,6 +1136,9 @@ html.light .tgl.btn.on {
|
||||
background: #4a0;
|
||||
color: #fff;
|
||||
}
|
||||
html.light .tgl.btn.on:hover {
|
||||
background: #5c0;
|
||||
}
|
||||
html.light #srv_info {
|
||||
color: #c83;
|
||||
background: #eee;
|
||||
@@ -1130,7 +1164,7 @@ html.light #treeul a.hl:hover {
|
||||
html.light #tree li {
|
||||
border-color: #f7f7f7 #fff #ddd #fff;
|
||||
}
|
||||
html.light #tree a:hover {
|
||||
html.light #treeul a:hover {
|
||||
background: #fff;
|
||||
}
|
||||
html.light #tree ul {
|
||||
@@ -1535,6 +1569,88 @@ html.light #bbox-overlay figcaption a {
|
||||
|
||||
#op_up2k {
|
||||
padding: 0 1em 1em 1em;
|
||||
min-height: 0;
|
||||
transition: min-height .2s;
|
||||
}
|
||||
#drops {
|
||||
display: none;
|
||||
z-index: 3;
|
||||
background: rgba(48, 48, 48, 0.7);
|
||||
}
|
||||
#drops.vis,
|
||||
#drops .dropzone {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
#drops .dropdesc {
|
||||
position: fixed;
|
||||
display: table;
|
||||
left: 10%;
|
||||
width: 78%;
|
||||
height: 30%;
|
||||
margin: 0;
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
text-shadow: .05em .05em .1em #000;
|
||||
background: rgba(224, 224, 224, 0.2);
|
||||
box-shadow: 0 0 0 #999;
|
||||
border: .5em solid #999;
|
||||
border-radius: .5em;
|
||||
border-width: 1vw;
|
||||
color: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
#drops .dropdesc.hl.ok {
|
||||
border-color: #fff;
|
||||
box-shadow: 0 0 1em #cf5;
|
||||
background: rgba(24, 24, 24, 0.7);
|
||||
left: 8%;
|
||||
width: 82%;
|
||||
height: 32%;
|
||||
margin: -1vh 0;
|
||||
}
|
||||
#drops .dropdesc.hl.err {
|
||||
background: rgba(224, 32, 65, 0.2);
|
||||
box-shadow: 0 0 1em #f26;
|
||||
border-color: #fab;
|
||||
}
|
||||
#drops .dropdesc>div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
#drops .dropzone {
|
||||
z-index: 80386;
|
||||
height: 50%;
|
||||
}
|
||||
#drops #up_dz {
|
||||
bottom: 50%;
|
||||
}
|
||||
#drops #srch_dz {
|
||||
top: 50%;
|
||||
}
|
||||
#up_zd {
|
||||
top: 12%;
|
||||
}
|
||||
#srch_zd {
|
||||
bottom: 12%;
|
||||
}
|
||||
.dropdesc span {
|
||||
color: #fff;
|
||||
background: #490;
|
||||
border-bottom: .3em solid #6d2;
|
||||
text-shadow: 1px 1px 1px #000;
|
||||
border-radius: .3em;
|
||||
padding: .4em .8em;
|
||||
font-size: .4em;
|
||||
}
|
||||
.dropdesc.err span {
|
||||
background: #904;
|
||||
border-color: #d26;
|
||||
}
|
||||
#u2form {
|
||||
position: absolute;
|
||||
@@ -1558,6 +1674,9 @@ html.light #bbox-overlay figcaption a {
|
||||
padding: .5em;
|
||||
font-size: .9em;
|
||||
}
|
||||
html.light #u2err.err {
|
||||
color: #f07;
|
||||
}
|
||||
#u2btn {
|
||||
color: #eee;
|
||||
background: #555;
|
||||
|
||||
@@ -133,7 +133,8 @@
|
||||
have_mv = {{ have_mv|tojson }},
|
||||
have_del = {{ have_del|tojson }},
|
||||
have_unpost = {{ have_unpost|tojson }},
|
||||
have_zip = {{ have_zip|tojson }};
|
||||
have_zip = {{ have_zip|tojson }},
|
||||
readme = {{ readme|tojson }};
|
||||
</script>
|
||||
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||
<script src="/.cpr/browser.js?_={{ ts }}"></script>
|
||||
|
||||
@@ -9,7 +9,7 @@ function dbg(msg) {
|
||||
|
||||
// toolbar
|
||||
ebi('ops').innerHTML = (
|
||||
'<a href="#" data-dest="" tt="close submenu">---</a>\n' +
|
||||
'<a href="#" data-dest="" tt="close submenu">--</a>\n' +
|
||||
(have_up2k_idx ? (
|
||||
'<a href="#" data-perm="read" data-dest="search" tt="search for files by attributes, path/name, music tags, or any combination of those.$N$N<code>foo bar</code> = must contain both foo and bar,$N<code>foo -bar</code> = must contain foo but not bar,$N<code>^yana .opus$</code> = must start with yana and have the opus extension">🔎</a>\n' +
|
||||
(have_del && have_unpost ? '<a href="#" data-dest="unpost" tt="unpost: delete your recent uploads">🧯</a>\n' : '') +
|
||||
@@ -40,8 +40,8 @@ ebi('widget').innerHTML = (
|
||||
' href="#" id="selinv" tt="invert selection">sel.<br />inv.</a><a' +
|
||||
' href="#" id="selzip" tt="download selection as archive">zip</a>' +
|
||||
'</span><span id="wnp"><a' +
|
||||
' href="#" id="npirc" tt="copy irc-formatted track info">📋irc</a><a' +
|
||||
' href="#" id="nptxt" tt="copy plaintext track info">📋txt</a>' +
|
||||
' href="#" id="npirc" tt="copy irc-formatted track info">📋<span>irc</span></a><a' +
|
||||
' href="#" id="nptxt" tt="copy plaintext track info">📋<span>txt</span></a>' +
|
||||
'</span><a' +
|
||||
' href="#" id="wtgrid" tt="toggle grid/list view$NHotkey: G">田</a><a' +
|
||||
' href="#" id="wtico">♫</a>' +
|
||||
@@ -133,6 +133,20 @@ ebi('op_up2k').innerHTML = (
|
||||
);
|
||||
|
||||
|
||||
(function () {
|
||||
var o = mknod('div');
|
||||
o.innerHTML = (
|
||||
'<div id="drops">\n' +
|
||||
' <div class="dropdesc" id="up_zd"><div>🚀 Upload<br /><span></span></div></div>\n' +
|
||||
' <div class="dropdesc" id="srch_zd"><div>🔎 Search<br /><span></span></div></div>\n' +
|
||||
' <div class="dropzone" id="up_dz" v="up_zd"></div>\n' +
|
||||
' <div class="dropzone" id="srch_dz" v="srch_zd"></div>\n' +
|
||||
'</div>'
|
||||
);
|
||||
document.body.appendChild(o);
|
||||
})();
|
||||
|
||||
|
||||
// config panel
|
||||
ebi('op_cfg').innerHTML = (
|
||||
'<div>\n' +
|
||||
@@ -140,9 +154,10 @@ ebi('op_cfg').innerHTML = (
|
||||
' <div>\n' +
|
||||
' <a id="tooltips" class="tgl btn" href="#" tt="◔ ◡ ◔">ℹ️ tooltips</a>\n' +
|
||||
' <a id="lightmode" class="tgl btn" href="#">☀️ lightmode</a>\n' +
|
||||
' <a id="dotfiles" class="tgl btn" href="#" tt="show hidden files (if server permits)">dotfiles</a>\n' +
|
||||
' <a id="griden" class="tgl btn" href="#" tt="toggle icons or list-view$NHotkey: G">田 the grid</a>\n' +
|
||||
' <a id="thumbs" class="tgl btn" href="#" tt="in icon view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs</a>\n' +
|
||||
' <a id="dotfiles" class="tgl btn" href="#" tt="show hidden files (if server permits)">dotfiles</a>\n' +
|
||||
' <a id="ireadme" class="tgl btn" href="#" tt="show README.md in folder listings">📜 readme</a>\n' +
|
||||
' </div>\n' +
|
||||
'</div>\n' +
|
||||
(have_zip ? (
|
||||
@@ -164,10 +179,12 @@ ebi('op_cfg').innerHTML = (
|
||||
ebi('tree').innerHTML = (
|
||||
'<div id="treeh">\n' +
|
||||
' <a href="#" id="detree" tt="show breadcrumbs$NHotkey: B">🍞...</a>\n' +
|
||||
' <a href="#" class="btn" step="2" id="twobytwo" tt="Hotkey: A">+</a>\n' +
|
||||
' <a href="#" class="btn" step="-2" id="twig" tt="Hotkey: D">–</a>\n' +
|
||||
' <a href="#" class="btn" step="2" id="twobytwo" tt="Hotkey: D">+</a>\n' +
|
||||
' <a href="#" class="btn" step="-2" id="twig" tt="Hotkey: A">–</a>\n' +
|
||||
' <a href="#" class="btn" id="visdir" tt="scroll to selected folder">🎯</a>\n' +
|
||||
' <a href="#" class="tgl btn" id="dyntree" tt="autogrow as tree expands">a</a>\n' +
|
||||
' <a href="#" class="btn" id="visdir" tt="scroll to selected folder">v</a>\n' +
|
||||
' <a href="#" class="tgl btn" id="wraptree" tt="word wrap">↵</a>\n' +
|
||||
' <a href="#" class="tgl btn" id="hovertree" tt="reveal overflowing lines on hover$N( breaks scrolling unless mouse $N cursor is in the left gutter )">👀</a>\n' +
|
||||
'</div>\n' +
|
||||
'<ul id="treeul"></ul>\n' +
|
||||
'<div id="thx_ff"> </div>'
|
||||
@@ -184,6 +201,9 @@ ebi('tree').innerHTML = (
|
||||
|
||||
function opclick(e) {
|
||||
var dest = this.getAttribute('data-dest');
|
||||
if (QS('#op_' + dest + '.act'))
|
||||
dest = '';
|
||||
|
||||
swrite('opmode', dest || null);
|
||||
if (ctrl(e))
|
||||
return;
|
||||
@@ -209,9 +229,22 @@ function goto(dest) {
|
||||
clmod(obj[a], 'act');
|
||||
|
||||
if (dest) {
|
||||
var ui = ebi('op_' + dest);
|
||||
var ui = ebi('op_' + dest),
|
||||
lnk = QS('#ops>a[data-dest=' + dest + ']'),
|
||||
nps = lnk.getAttribute('data-perm');
|
||||
|
||||
nps = nps && nps.length ? nps.split(' ') : [];
|
||||
|
||||
if (perms.length)
|
||||
for (var a = 0; a < nps.length; a++)
|
||||
if (!has(perms, nps[a]))
|
||||
return;
|
||||
|
||||
if (!has(perms, 'read') && !has(perms, 'write') && (dest == 'up2k'))
|
||||
return;
|
||||
|
||||
clmod(ui, 'act', true);
|
||||
QS('#ops>a[data-dest=' + dest + ']').className += " act";
|
||||
lnk.className += " act";
|
||||
|
||||
var fn = window['goto_' + dest];
|
||||
if (fn)
|
||||
@@ -260,24 +293,13 @@ var mpl = (function () {
|
||||
|
||||
var r = {
|
||||
"pb_mode": sread('pb_mode') || 'loop-folder',
|
||||
"preload": bcfg_get('au_preload', true),
|
||||
"clip": bcfg_get('au_npclip', false),
|
||||
"os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl,
|
||||
"osd_cv": bcfg_get('au_osd_cv', true),
|
||||
};
|
||||
|
||||
ebi('au_preload').onclick = function (e) {
|
||||
ev(e);
|
||||
r.preload = !r.preload;
|
||||
bcfg_set('au_preload', r.preload);
|
||||
};
|
||||
|
||||
ebi('au_npclip').onclick = function (e) {
|
||||
ev(e);
|
||||
r.clip = !r.clip;
|
||||
bcfg_set('au_npclip', r.clip);
|
||||
clmod(ebi('wtoggle'), 'np', r.clip && mp.au);
|
||||
};
|
||||
bcfg_bind(r, 'preload', 'au_preload', true);
|
||||
bcfg_bind(r, 'osd_cv', 'au_osd_cv', true);
|
||||
bcfg_bind(r, 'clip', 'au_npclip', false, function (v) {
|
||||
clmod(ebi('wtoggle'), 'np', v && mp.au);
|
||||
});
|
||||
|
||||
ebi('au_os_ctl').onclick = function (e) {
|
||||
ev(e);
|
||||
@@ -287,12 +309,6 @@ var mpl = (function () {
|
||||
toast.err(5, 'need firefox 82+ or chrome 73+\n(or iOS 15+ supposedly)');
|
||||
};
|
||||
|
||||
ebi('au_osd_cv').onclick = function (e) {
|
||||
ev(e);
|
||||
r.osd_cv = !r.osd_cv;
|
||||
bcfg_set('au_osd_cv', r.osd_cv);
|
||||
};
|
||||
|
||||
function draw_pb_mode() {
|
||||
var btns = QSA('#pb_mode>a');
|
||||
for (var a = 0, aa = btns.length; a < aa; a++) {
|
||||
@@ -352,7 +368,7 @@ var mpl = (function () {
|
||||
|
||||
for (var a = 0, aa = files.length; a < aa; a++) {
|
||||
if (/^(cover|folder)\.(jpe?g|png|gif)$/.test(files[a].textContent)) {
|
||||
cover = files[a].getAttribute('href');
|
||||
cover = noq_href(files[a]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -411,7 +427,7 @@ function MPlayer() {
|
||||
link = tds[1].getElementsByTagName('a');
|
||||
|
||||
link = link[link.length - 1];
|
||||
var url = link.getAttribute('href'),
|
||||
var url = noq_href(link),
|
||||
m = re_audio.exec(url);
|
||||
|
||||
if (m) {
|
||||
@@ -717,6 +733,10 @@ var pbar = (function () {
|
||||
for (var p = 1, mins = adur / 60; p <= mins; p++)
|
||||
pctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h);
|
||||
|
||||
pctx.fillStyle = light ? 'rgba(0,0,0,1)' : 'rgba(255,255,255,1)';
|
||||
for (var p = 1, mins = adur / 600; p <= mins; p++)
|
||||
pctx.fillRect(Math.floor(sm * p * 600), 0, 2, pc.h);
|
||||
|
||||
var w = 8,
|
||||
x = sm * apos;
|
||||
|
||||
@@ -1044,6 +1064,33 @@ var audio_eq = (function () {
|
||||
"last_au": null
|
||||
};
|
||||
|
||||
// some browsers have insane high-frequency boost
|
||||
// (or rather the actual problem is Q but close enough)
|
||||
r.cali = (function () {
|
||||
try {
|
||||
var ac = new AudioContext(),
|
||||
fi = ac.createBiquadFilter(),
|
||||
freqs = new Float32Array(1),
|
||||
mag = new Float32Array(1),
|
||||
phase = new Float32Array(1);
|
||||
|
||||
freqs[0] = 14000;
|
||||
fi.type = 'peaking';
|
||||
fi.frequency.value = 18000;
|
||||
fi.Q.value = 0.8;
|
||||
fi.gain.value = 1;
|
||||
fi.getFrequencyResponse(freqs, mag, phase);
|
||||
|
||||
return mag[0]; // 1.0407 good, 1.0563 bad
|
||||
}
|
||||
catch (ex) {
|
||||
return 0;
|
||||
}
|
||||
})();
|
||||
console.log('eq cali: ' + r.cali);
|
||||
|
||||
var e1 = r.cali < 1.05;
|
||||
|
||||
var cfg = [ // hz, q, g
|
||||
[31.25 * 0.88, 0, 1.4], // shelf
|
||||
[31.25 * 1.04, 0.7, 0.96], // peak
|
||||
@@ -1054,10 +1101,10 @@ var audio_eq = (function () {
|
||||
[1000, 0.9, 1.1],
|
||||
[2000, 0.9, 1.105],
|
||||
[4000, 0.88, 1.05],
|
||||
[8000 * 1.006, 0.73, 1.24],
|
||||
[16000 * 0.89, 0.7, 1.26], // peak
|
||||
[16000 * 1.13, 0.82, 1.09], // peak
|
||||
[16000 * 1.205, 0, 1.9] // shelf
|
||||
[8000 * 1.006, 0.73, e1 ? 1.24 : 1.2],
|
||||
[16000 * 0.89, 0.7, e1 ? 1.26 : 1.2], // peak
|
||||
[16000 * 1.13, 0.82, e1 ? 1.09 : 0.75], // peak
|
||||
[16000 * 1.205, 0, e1 ? 1.9 : 1.85] // shelf
|
||||
];
|
||||
|
||||
try {
|
||||
@@ -1234,13 +1281,7 @@ var audio_eq = (function () {
|
||||
txt[a].onkeydown = eq_keydown;
|
||||
}
|
||||
|
||||
r.en = bcfg_get('au_eq', false);
|
||||
ebi('au_eq').onclick = function (e) {
|
||||
ev(e);
|
||||
r.en = !r.en;
|
||||
bcfg_set('au_eq', r.en);
|
||||
r.apply();
|
||||
};
|
||||
bcfg_bind(r, 'en', 'au_eq', false, r.apply);
|
||||
|
||||
r.draw();
|
||||
return r;
|
||||
@@ -1252,6 +1293,9 @@ function play(tid, is_ev, seek, call_depth) {
|
||||
if (mp.order.length == 0)
|
||||
return console.log('no audio found wait what');
|
||||
|
||||
if (crashed)
|
||||
return;
|
||||
|
||||
mp.stopfade(true);
|
||||
|
||||
var tn = tid;
|
||||
@@ -1472,6 +1516,7 @@ function autoplay_blocked(seek) {
|
||||
go.textContent = 'Play "' + fn + '"';
|
||||
go.onclick = function (e) {
|
||||
unblocked(e);
|
||||
toast.hide();
|
||||
if (mp.au !== mp.au_ogvjs)
|
||||
// chrome 91 may permanently taint on a failed play()
|
||||
// depending on win10 settings or something? idk
|
||||
@@ -1548,7 +1593,7 @@ function sortfiles(nodes) {
|
||||
if ((v + '').indexOf('<a ') === 0)
|
||||
v = v.split('>')[1];
|
||||
else if (name == "href" && v) {
|
||||
if (v.slice(-1) == '/')
|
||||
if (v.split('?')[0].slice(-1) == '/')
|
||||
v = '\t' + v;
|
||||
|
||||
v = uricom_dec(v)[0]
|
||||
@@ -1718,7 +1763,7 @@ var fileman = (function () {
|
||||
bcut.style.display = have_mv && has(perms, 'move') ? '' : 'none';
|
||||
bpst.style.display = have_mv && has(perms, 'write') ? '' : 'none';
|
||||
bpst.setAttribute('tt', 'paste ' + r.clip.length + ' items$NHotkey: ctrl-V');
|
||||
ebi('wfm').style.display = QS('#wfm a.en:not([display])') ? '' : 'none';
|
||||
clmod(ebi('wfm'), 'act', QS('#wfm a.en:not([style])'));
|
||||
};
|
||||
|
||||
r.rename = function (e) {
|
||||
@@ -1785,7 +1830,7 @@ var fileman = (function () {
|
||||
|
||||
html = html.concat([
|
||||
'<button id="rn_cancel" tt="abort and close this window">❌ cancel</button>',
|
||||
'<button id="rn_apply">✅ apply rename</button>',
|
||||
'<button id="rn_apply">✅ APPLY RENAME</button>',
|
||||
'<a id="rn_adv" class="tgl btn" href="#" tt="batch / metadata / pattern renaming">advanced</a>',
|
||||
'<a id="rn_case" class="tgl btn" href="#" tt="case-sensitive regex">case</a>',
|
||||
'</div>',
|
||||
@@ -1859,12 +1904,11 @@ var fileman = (function () {
|
||||
rn_reset(0);
|
||||
tt.att(rui);
|
||||
|
||||
var adv = bcfg_get('rn_adv', false),
|
||||
cs = bcfg_get('rn_case', false);
|
||||
|
||||
function sadv() {
|
||||
ebi('rn_vadv').style.display = ebi('rn_case').style.display = adv ? '' : 'none';
|
||||
ebi('rn_vadv').style.display = ebi('rn_case').style.display = r.adv ? '' : 'none';
|
||||
}
|
||||
bcfg_bind(r, 'adv', 'rn_adv', false, sadv);
|
||||
bcfg_bind(r, 'cs', 'rn_case', false);
|
||||
sadv();
|
||||
|
||||
function rn_ok(n, ok) {
|
||||
@@ -1877,21 +1921,13 @@ var fileman = (function () {
|
||||
f[n].inew.focus();
|
||||
f[n].inew.setSelectionRange(0, f[n].inew.value.lastIndexOf('.'), "forward");
|
||||
}
|
||||
function rn_cancel() {
|
||||
function rn_cancel(e) {
|
||||
ev(e);
|
||||
rui.parentNode.removeChild(rui);
|
||||
}
|
||||
|
||||
ebi('rn_cancel').onclick = rn_cancel;
|
||||
ebi('rn_apply').onclick = rn_apply;
|
||||
ebi('rn_adv').onclick = function () {
|
||||
adv = !adv;
|
||||
bcfg_set('rn_adv', adv);
|
||||
sadv();
|
||||
};
|
||||
ebi('rn_case').onclick = function () {
|
||||
cs = !cs;
|
||||
bcfg_set('rn_case', cs);
|
||||
};
|
||||
|
||||
var ire = ebi('rn_re'),
|
||||
ifmt = ebi('rn_fmt'),
|
||||
@@ -1915,7 +1951,8 @@ var fileman = (function () {
|
||||
ipre.appendChild(o);
|
||||
}
|
||||
}
|
||||
inew.onclick = function () {
|
||||
inew.onclick = function (e) {
|
||||
ev(e);
|
||||
modal.prompt('provide a name for your new preset', ifmt.value, function (name) {
|
||||
if (!name)
|
||||
return toast.warn(3, 'aborted');
|
||||
@@ -1926,7 +1963,8 @@ var fileman = (function () {
|
||||
ipre.value = name;
|
||||
});
|
||||
};
|
||||
idel.onclick = function () {
|
||||
idel.onclick = function (e) {
|
||||
ev(e);
|
||||
delete presets[ipre.value];
|
||||
jwrite('rn_pre', presets);
|
||||
spresets();
|
||||
@@ -1959,7 +1997,7 @@ var fileman = (function () {
|
||||
|
||||
try {
|
||||
if (ptn)
|
||||
re = new RegExp(ptn, cs ? 'i' : '');
|
||||
re = new RegExp(ptn, r.cs ? 'i' : '');
|
||||
}
|
||||
catch (ex) {
|
||||
return toast.err(5, esc('invalid regex:\n' + ex));
|
||||
@@ -1984,7 +2022,8 @@ var fileman = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
function rn_apply() {
|
||||
function rn_apply(e) {
|
||||
ev(e);
|
||||
while (f.length && (!f[0].ok || f[0].ofn == f[0].inew.value))
|
||||
f.shift();
|
||||
|
||||
@@ -2089,9 +2128,18 @@ var fileman = (function () {
|
||||
clmod(els[a], 'fcut', 1);
|
||||
}, 1);
|
||||
|
||||
toast.inf(1.5, 'cut ' + sel.length + ' items');
|
||||
jwrite('fman_clip', vps);
|
||||
r.tx(1);
|
||||
try {
|
||||
vps = JSON.stringify(vps);
|
||||
if (vps.length > 1024 * 1024)
|
||||
throw 'a';
|
||||
|
||||
swrite('fman_clip', vps);
|
||||
r.tx(1);
|
||||
toast.inf(1.5, 'cut ' + sel.length + ' items');
|
||||
}
|
||||
catch (ex) {
|
||||
toast.warn(30, 'cut ' + sel.length + ' items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)');
|
||||
}
|
||||
};
|
||||
|
||||
r.paste = function (e) {
|
||||
@@ -2109,7 +2157,7 @@ var fileman = (function () {
|
||||
links = QSA('#files tbody td:nth-child(2) a');
|
||||
|
||||
for (var a = 0, aa = links.length; a < aa; a++)
|
||||
indir.push(vsplit(links[a].getAttribute('href'))[1]);
|
||||
indir.push(vsplit(noq_href(links[a]))[1]);
|
||||
|
||||
for (var a = 0; a < r.clip.length; a++) {
|
||||
var found = false;
|
||||
@@ -2217,36 +2265,12 @@ var thegrid = (function () {
|
||||
lfiles.parentNode.insertBefore(gfiles, lfiles);
|
||||
|
||||
var r = {
|
||||
'thumbs': bcfg_get('thumbs', true),
|
||||
'en': bcfg_get('griden', false),
|
||||
'sel': bcfg_get('gridsel', false),
|
||||
'sz': clamp(fcfg_get('gridsz', 10), 4, 40),
|
||||
'ln': clamp(icfg_get('gridln', 3), 1, 7),
|
||||
'isdirty': true,
|
||||
'bbox': null
|
||||
};
|
||||
|
||||
ebi('thumbs').onclick = function (e) {
|
||||
ev(e);
|
||||
r.thumbs = !r.thumbs;
|
||||
bcfg_set('thumbs', r.thumbs);
|
||||
r.setdirty();
|
||||
};
|
||||
|
||||
ebi('griden').onclick = ebi('wtgrid').onclick = function (e) {
|
||||
ev(e);
|
||||
r.en = !r.en;
|
||||
bcfg_set('griden', r.en);
|
||||
if (r.en) {
|
||||
loadgrid();
|
||||
}
|
||||
else {
|
||||
ungrid();
|
||||
}
|
||||
pbar.onresize();
|
||||
vbar.onresize();
|
||||
};
|
||||
|
||||
var btnclick = function (e) {
|
||||
ev(e);
|
||||
var s = this.getAttribute('s'),
|
||||
@@ -2273,12 +2297,14 @@ var thegrid = (function () {
|
||||
for (var a = 0; a < links.length; a++)
|
||||
links[a].onclick = btnclick;
|
||||
|
||||
ebi('gridsel').onclick = function (e) {
|
||||
ev(e);
|
||||
r.sel = !r.sel;
|
||||
bcfg_set('gridsel', r.sel);
|
||||
r.loadsel();
|
||||
};
|
||||
bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty);
|
||||
bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel);
|
||||
bcfg_bind(r, 'en', 'griden', false, function (v) {
|
||||
v ? loadgrid() : ungrid();
|
||||
pbar.onresize();
|
||||
vbar.onresize();
|
||||
});
|
||||
ebi('wtgrid').onclick = ebi('griden').onclick;
|
||||
|
||||
r.setvis = function (vis) {
|
||||
(r.en ? gfiles : lfiles).style.display = vis ? '' : 'none';
|
||||
@@ -2324,7 +2350,7 @@ var thegrid = (function () {
|
||||
return true;
|
||||
|
||||
var oth = ebi(this.getAttribute('ref')),
|
||||
href = this.getAttribute('href'),
|
||||
href = noq_href(this),
|
||||
aplay = ebi('a' + oth.getAttribute('id')),
|
||||
is_img = /\.(gif|jpe?g|png|webp|webm|mp4)(\?|$)/i.test(href),
|
||||
in_tree = null,
|
||||
@@ -2332,7 +2358,7 @@ var thegrid = (function () {
|
||||
td = oth.closest('td').nextSibling,
|
||||
tr = td.parentNode;
|
||||
|
||||
if (/\/(\?|$)/.test(href)) {
|
||||
if (href.endsWith('/')) {
|
||||
var ta = QSA('#treeul a.hl+ul>li>a+a'),
|
||||
txt = oth.textContent.slice(0, -1);
|
||||
|
||||
@@ -2371,14 +2397,15 @@ var thegrid = (function () {
|
||||
var tr = ebi(ths[a].getAttribute('ref')).closest('tr'),
|
||||
cl = tr.getAttribute('class') || '';
|
||||
|
||||
if (ths[a].getAttribute('href').endsWith('/'))
|
||||
if (noq_href(ths[a]).endsWith('/'))
|
||||
cl += ' dir';
|
||||
|
||||
ths[a].setAttribute('class', cl);
|
||||
}
|
||||
var uns = QS('#ggrid a[ref="unsearch"]');
|
||||
if (uns)
|
||||
uns.onclick = function () {
|
||||
uns.onclick = function (e) {
|
||||
ev(e);
|
||||
ebi('unsearch').click();
|
||||
};
|
||||
};
|
||||
@@ -2434,15 +2461,16 @@ var thegrid = (function () {
|
||||
var files = QSA('#files>tbody>tr>td:nth-child(2) a[id]');
|
||||
for (var a = 0, aa = files.length; a < aa; a++) {
|
||||
var ao = files[a],
|
||||
href = esc(ao.getAttribute('href')),
|
||||
ohref = esc(ao.getAttribute('href')),
|
||||
href = ohref.split('?')[0],
|
||||
name = uricom_dec(vsplit(href)[1])[0],
|
||||
ref = ao.getAttribute('id'),
|
||||
isdir = href.split('?')[0].slice(-1)[0] == '/',
|
||||
isdir = href.endsWith('/'),
|
||||
ac = isdir ? ' class="dir"' : '',
|
||||
ihref = href;
|
||||
|
||||
if (r.thumbs) {
|
||||
ihref += (ihref.indexOf('?') === -1 ? '?' : '&') + 'th=' + (have_webp ? 'w' : 'j');
|
||||
ihref += '?th=' + (have_webp ? 'w' : 'j');
|
||||
if (href == "#")
|
||||
ihref = '/.cpr/ico/⏏️';
|
||||
}
|
||||
@@ -2450,7 +2478,7 @@ var thegrid = (function () {
|
||||
ihref = '/.cpr/ico/folder';
|
||||
}
|
||||
else {
|
||||
var ar = href.split('?')[0].split('.');
|
||||
var ar = href.split('.');
|
||||
if (ar.length > 1)
|
||||
ar = ar.slice(1);
|
||||
|
||||
@@ -2468,7 +2496,7 @@ var thegrid = (function () {
|
||||
ihref = '/.cpr/ico/' + ihref.slice(0, -1);
|
||||
}
|
||||
|
||||
html.push('<a href="' + href + '" ref="' + ref +
|
||||
html.push('<a href="' + ohref + '" ref="' + ref +
|
||||
'"' + ac + ' ttt="' + esc(name) + '"><img src="' +
|
||||
ihref + '" /><span' + ac + '>' + ao.innerHTML + '</span></a>');
|
||||
}
|
||||
@@ -2700,10 +2728,10 @@ document.onkeydown = function (e) {
|
||||
return ebi('gridsel').click();
|
||||
|
||||
if (k == 'KeyA')
|
||||
return QSA('#ghead>a[z]')[0].click();
|
||||
return QSA('#ghead a[z]')[0].click();
|
||||
|
||||
if (k == 'KeyD')
|
||||
return QSA('#ghead>a[z]')[1].click();
|
||||
return QSA('#ghead a[z]')[1].click();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2796,14 +2824,15 @@ document.onkeydown = function (e) {
|
||||
|
||||
clearTimeout(defer_timeout);
|
||||
defer_timeout = setTimeout(try_search, 2000);
|
||||
try_search();
|
||||
try_search(v);
|
||||
}
|
||||
|
||||
function try_search() {
|
||||
function try_search(v) {
|
||||
if (Date.now() - search_in_progress > 30 * 1000) {
|
||||
clearTimeout(defer_timeout);
|
||||
clearTimeout(search_timeout);
|
||||
search_timeout = setTimeout(do_search, 200);
|
||||
search_timeout = setTimeout(do_search,
|
||||
v && v.length < (is_touch ? 4 : 3) ? 600 : 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2980,15 +3009,27 @@ var treectl = (function () {
|
||||
var treectl = {
|
||||
"hidden": true,
|
||||
"ls_cb": null,
|
||||
"dir_cb": tree_scrollto
|
||||
"dir_cb": tree_scrollto,
|
||||
},
|
||||
entreed = false,
|
||||
fixedpos = false,
|
||||
prev_atop = null,
|
||||
prev_winh = null,
|
||||
dyn = bcfg_get('dyntree', true),
|
||||
dots = bcfg_get('dotfiles', false),
|
||||
treesz = clamp(icfg_get('treesz', 16), 4, 50);
|
||||
mentered = null,
|
||||
treesz = clamp(icfg_get('treesz', 16), 10, 50);
|
||||
|
||||
bcfg_bind(treectl, 'ireadme', 'ireadme', true);
|
||||
bcfg_bind(treectl, 'dyn', 'dyntree', true, onresize);
|
||||
bcfg_bind(treectl, 'dots', 'dotfiles', false, function (v) {
|
||||
treectl.goto(get_evpath());
|
||||
});
|
||||
setwrap(bcfg_bind(treectl, 'wtree', 'wraptree', true, setwrap));
|
||||
bcfg_bind(treectl, 'htree', 'hovertree', true, reload_tree);
|
||||
|
||||
function setwrap(v) {
|
||||
clmod(ebi('tree'), 'nowrap', !v);
|
||||
reload_tree();
|
||||
}
|
||||
|
||||
treectl.entree = function (e) {
|
||||
ev(e);
|
||||
@@ -3030,7 +3071,19 @@ var treectl = (function () {
|
||||
window.removeEventListener('scroll', onscroll);
|
||||
}
|
||||
|
||||
function unmenter() {
|
||||
if (mentered) {
|
||||
mentered.style.position = '';
|
||||
mentered = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onscroll() {
|
||||
unmenter();
|
||||
onscroll2();
|
||||
}
|
||||
|
||||
function onscroll2() {
|
||||
if (!entreed || treectl.hidden || document.visibilityState == 'hidden')
|
||||
return;
|
||||
|
||||
@@ -3067,24 +3120,28 @@ var treectl = (function () {
|
||||
tree.style.height = treeh < 10 ? '' : treeh + 'px';
|
||||
}
|
||||
}
|
||||
timer.add(onscroll, true);
|
||||
timer.add(onscroll2, true);
|
||||
|
||||
function onresize(e) {
|
||||
if (!entreed || treectl.hidden)
|
||||
return;
|
||||
|
||||
var q = '#tree',
|
||||
nq = 0;
|
||||
nq = -3;
|
||||
|
||||
while (dyn) {
|
||||
while (treectl.dyn) {
|
||||
nq++;
|
||||
q += '>ul>li';
|
||||
if (!QS(q))
|
||||
break;
|
||||
}
|
||||
var w = treesz + nq;
|
||||
ebi('tree').style.width = w + 'em';
|
||||
ebi('wrap').style.marginLeft = w + 'em';
|
||||
var w = (treesz + Math.max(0, nq)) + 'em';
|
||||
try {
|
||||
document.documentElement.style.setProperty('--nav-sz', w);
|
||||
}
|
||||
catch (ex) { }
|
||||
ebi('tree').style.width = w;
|
||||
ebi('wrap').style.marginLeft = w;
|
||||
onscroll();
|
||||
}
|
||||
|
||||
@@ -3099,7 +3156,7 @@ var treectl = (function () {
|
||||
xhr.dst = dst;
|
||||
xhr.rst = rst;
|
||||
xhr.ts = Date.now();
|
||||
xhr.open('GET', dst + '?tree=' + top + (dots ? '&dots' : ''), true);
|
||||
xhr.open('GET', dst + '?tree=' + top + (treectl.dots ? '&dots' : ''), true);
|
||||
xhr.onreadystatechange = recvtree;
|
||||
xhr.send();
|
||||
enspin('#tree');
|
||||
@@ -3171,18 +3228,38 @@ var treectl = (function () {
|
||||
|
||||
function reload_tree() {
|
||||
var cdir = get_evpath(),
|
||||
links = QSA('#treeul a+a');
|
||||
links = QSA('#treeul a+a'),
|
||||
nowrap = QS('#tree.nowrap') && QS('#hovertree.on');
|
||||
|
||||
for (var a = 0, aa = links.length; a < aa; a++) {
|
||||
var href = links[a].getAttribute('href');
|
||||
links[a].setAttribute('class', href == cdir ? 'hl' : '');
|
||||
links[a].onclick = treego;
|
||||
links[a].onmouseenter = nowrap ? menter : null;
|
||||
links[a].onmouseleave = nowrap ? mleave : null;
|
||||
}
|
||||
links = QSA('#treeul li>a:first-child');
|
||||
for (var a = 0, aa = links.length; a < aa; a++) {
|
||||
links[a].setAttribute('dst', links[a].nextSibling.getAttribute('href'));
|
||||
links[a].onclick = treegrow;
|
||||
}
|
||||
ebi('tree').onscroll = nowrap ? unmenter : null;
|
||||
}
|
||||
|
||||
function menter(e) {
|
||||
var p = this.offsetParent,
|
||||
pp = p.offsetParent,
|
||||
ppy = pp.offsetTop,
|
||||
y = this.offsetTop + p.offsetTop + ppy - p.scrollTop - pp.scrollTop - (ppy ? document.documentElement.scrollTop : 0);
|
||||
|
||||
this.style.top = y + 'px';
|
||||
this.style.position = 'fixed';
|
||||
mentered = this;
|
||||
}
|
||||
|
||||
function mleave(e) {
|
||||
this.style.position = '';
|
||||
mentered = null;
|
||||
}
|
||||
|
||||
function treego(e) {
|
||||
@@ -3203,7 +3280,7 @@ var treectl = (function () {
|
||||
xhr.top = url;
|
||||
xhr.hpush = hpush;
|
||||
xhr.ts = Date.now();
|
||||
xhr.open('GET', xhr.top + '?ls' + (dots ? '&dots' : ''), true);
|
||||
xhr.open('GET', xhr.top + '?ls' + (treectl.dots ? '&dots' : ''), true);
|
||||
xhr.onreadystatechange = recvls;
|
||||
xhr.send();
|
||||
if (hpush)
|
||||
@@ -3261,8 +3338,9 @@ var treectl = (function () {
|
||||
nodes = sortfiles(nodes);
|
||||
for (var a = 0; a < nodes.length; a++) {
|
||||
var r = nodes[a],
|
||||
hname = esc(uricom_dec(r.href)[0]),
|
||||
sortv = (r.href.slice(-1) == '/' ? '\t' : '') + hname,
|
||||
bhref = r.href.split('?')[0],
|
||||
hname = esc(uricom_dec(bhref)[0]),
|
||||
sortv = (bhref.slice(-1) == '/' ? '\t' : '') + hname,
|
||||
ln = ['<tr><td>' + r.lead + '</td><td sortv="' + sortv +
|
||||
'"><a href="' + top + r.href + '">' + hname + '</a>', r.sz];
|
||||
|
||||
@@ -3301,6 +3379,10 @@ var treectl = (function () {
|
||||
ebi('pro').innerHTML = res.logues ? res.logues[0] || "" : "";
|
||||
ebi('epi').innerHTML = res.logues ? res.logues[1] || "" : "";
|
||||
|
||||
clmod(ebi('epi'), 'mdo');
|
||||
if (res.readme)
|
||||
show_readme(res.readme);
|
||||
|
||||
document.title = '⇆🎉 ' + uricom_dec(document.location.pathname.slice(1, -1))[0];
|
||||
|
||||
filecols.set_style();
|
||||
@@ -3346,26 +3428,13 @@ var treectl = (function () {
|
||||
return ret;
|
||||
}
|
||||
|
||||
function tdots(e) {
|
||||
ev(e);
|
||||
dots = !dots;
|
||||
bcfg_set('dotfiles', dots);
|
||||
treectl.goto(get_evpath());
|
||||
}
|
||||
|
||||
function dyntree(e) {
|
||||
ev(e);
|
||||
dyn = !dyn;
|
||||
bcfg_set('dyntree', dyn);
|
||||
onresize();
|
||||
}
|
||||
|
||||
function scaletree(e) {
|
||||
ev(e);
|
||||
treesz += parseInt(this.getAttribute("step"));
|
||||
if (isNaN(treesz))
|
||||
treesz = 16;
|
||||
|
||||
treesz = clamp(treesz, 2, 120);
|
||||
swrite('treesz', treesz);
|
||||
onresize();
|
||||
}
|
||||
@@ -3373,8 +3442,6 @@ var treectl = (function () {
|
||||
ebi('entree').onclick = treectl.entree;
|
||||
ebi('detree').onclick = treectl.detree;
|
||||
ebi('visdir').onclick = tree_scrollto;
|
||||
ebi('dotfiles').onclick = tdots;
|
||||
ebi('dyntree').onclick = dyntree;
|
||||
ebi('twig').onclick = scaletree;
|
||||
ebi('twobytwo').onclick = scaletree;
|
||||
if (sread('entreed') == 'tree')
|
||||
@@ -3420,7 +3487,7 @@ function apply_perms(newperms) {
|
||||
|
||||
var axs = [],
|
||||
aclass = '>',
|
||||
chk = ['read', 'write', 'move', 'delete'];
|
||||
chk = ['read', 'write', 'move', 'delete', 'get'];
|
||||
|
||||
for (var a = 0; a < chk.length; a++)
|
||||
if (has(perms, chk[a]))
|
||||
@@ -3474,7 +3541,7 @@ function apply_perms(newperms) {
|
||||
|
||||
ebi('widget').style.display = have_read ? '' : 'none';
|
||||
thegrid.setvis(have_read);
|
||||
if (!have_read)
|
||||
if (!have_read && have_write)
|
||||
goto('up2k');
|
||||
}
|
||||
|
||||
@@ -3575,6 +3642,7 @@ var filecols = (function () {
|
||||
if (ttv) {
|
||||
th.setAttribute("tt", ttv);
|
||||
th.setAttribute("ttd", "u");
|
||||
th.setAttribute("ttm", "12");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -3799,21 +3867,15 @@ function addcrc() {
|
||||
|
||||
var light;
|
||||
(function () {
|
||||
light = bcfg_get('lightmode', false);
|
||||
|
||||
function freshen() {
|
||||
clmod(document.documentElement, "light", light);
|
||||
clmod(document.documentElement, "dark", !light);
|
||||
pbar.drawbuf();
|
||||
pbar.drawpos();
|
||||
vbar.draw();
|
||||
}
|
||||
|
||||
ebi('lightmode').onclick = function (e) {
|
||||
ev(e);
|
||||
light = !light;
|
||||
bcfg_set('lightmode', light);
|
||||
freshen();
|
||||
};
|
||||
bcfg_bind(window, 'light', 'lightmode', false, freshen);
|
||||
|
||||
freshen();
|
||||
})();
|
||||
@@ -3921,7 +3983,7 @@ var msel = (function () {
|
||||
vbase = get_evpath();
|
||||
|
||||
for (var a = 0, aa = links.length; a < aa; a++) {
|
||||
var href = links[a].getAttribute('href').replace(/\/$/, ""),
|
||||
var href = noq_href(links[a]).replace(/\/$/, ""),
|
||||
item = {};
|
||||
|
||||
item.id = links[a].getAttribute('id');
|
||||
@@ -4016,14 +4078,59 @@ var msel = (function () {
|
||||
})();
|
||||
|
||||
|
||||
function show_readme(md, url, depth) {
|
||||
if (!treectl.ireadme)
|
||||
return;
|
||||
|
||||
var div = ebi('epi'),
|
||||
errmsg = 'cannot show README.md:\n\n',
|
||||
now = window.location.href.replace(/\/?[?#].*/, "");
|
||||
|
||||
url = url || now;
|
||||
if (url != now)
|
||||
return;
|
||||
|
||||
if (!window['marked']) {
|
||||
if (depth)
|
||||
return toast.warn(10, errmsg + 'failed to load marked.js')
|
||||
|
||||
return import_js('/.cpr/deps/marked.js', function () {
|
||||
show_readme(md, url, 1);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
clmod(div, 'mdo', 1);
|
||||
div.innerHTML = marked(md, {
|
||||
headerPrefix: 'md-',
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
var links = QSA('#epi a');
|
||||
for (var a = 0, aa = links.length; a < aa; a++) {
|
||||
var href = links[a].getAttribute('href');
|
||||
if (!href.startsWith('#'))
|
||||
continue;
|
||||
|
||||
links[a].setAttribute('href', '#md-' + href.slice(1));
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
toast.warn(10, errmsg + ex);
|
||||
}
|
||||
}
|
||||
if (readme)
|
||||
show_readme(readme);
|
||||
|
||||
|
||||
(function () {
|
||||
try {
|
||||
var tr = ebi('files').tBodies[0].rows;
|
||||
for (var a = 0; a < tr.length; a++) {
|
||||
var td = tr[a].cells[1],
|
||||
ao = td.firstChild,
|
||||
href = ao.getAttribute('href'),
|
||||
isdir = href.split('?')[0].slice(-1)[0] == '/',
|
||||
href = noq_href(ao),
|
||||
isdir = href.endsWith('/'),
|
||||
txt = ao.textContent;
|
||||
|
||||
td.setAttribute('sortv', (isdir ? '\t' : '') + txt);
|
||||
@@ -4041,7 +4148,7 @@ function ev_row_tgl(e) {
|
||||
|
||||
var unpost = (function () {
|
||||
ebi('op_unpost').innerHTML = (
|
||||
"you can delete your recent uploads below – click the fire-extinguisher icon to refresh" +
|
||||
'you can delete your recent uploads below – <a id="unpost_refresh" href="#">refresh list</a>' +
|
||||
'<p>optional filter: URL must contain <input type="text" id="unpost_filt" size="20" /><a id="unpost_nofilt" href="#">clear filter</a></p>' +
|
||||
'<div id="unpost"></div>'
|
||||
);
|
||||
@@ -4138,6 +4245,7 @@ var unpost = (function () {
|
||||
}
|
||||
|
||||
ct.onclick = function (e) {
|
||||
ev(e);
|
||||
var tgt = e.target.closest('a[me]');
|
||||
if (!tgt)
|
||||
return;
|
||||
@@ -4179,11 +4287,17 @@ var unpost = (function () {
|
||||
tfilt = setTimeout(r.load, 250);
|
||||
};
|
||||
|
||||
ebi('unpost_nofilt').onclick = function () {
|
||||
ebi('unpost_nofilt').onclick = function (e) {
|
||||
ev(e);
|
||||
filt.value = '';
|
||||
r.load();
|
||||
};
|
||||
|
||||
ebi('unpost_refresh').onclick = function (e) {
|
||||
ev(e);
|
||||
goto('unpost');
|
||||
};
|
||||
|
||||
return r;
|
||||
})();
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
@font-face {
|
||||
font-family: 'scp';
|
||||
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(/.cpr/deps/scp.woff2) format('woff2');
|
||||
}
|
||||
html, body {
|
||||
color: #333;
|
||||
background: #eee;
|
||||
@@ -11,7 +7,7 @@ html, body {
|
||||
#repl {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: .2em;
|
||||
right: .5em;
|
||||
border: none;
|
||||
color: inherit;
|
||||
background: none;
|
||||
@@ -27,122 +23,8 @@ html, body {
|
||||
bottom: auto;
|
||||
top: 1.4em;
|
||||
}
|
||||
pre, code, a {
|
||||
color: #480;
|
||||
background: #f7f7f7;
|
||||
border: .07em solid #ddd;
|
||||
border-radius: .2em;
|
||||
padding: .1em .3em;
|
||||
margin: 0 .1em;
|
||||
}
|
||||
code {
|
||||
font-size: .96em;
|
||||
}
|
||||
pre, code, tt {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
pre {
|
||||
counter-reset: precode;
|
||||
}
|
||||
pre code {
|
||||
counter-increment: precode;
|
||||
display: inline-block;
|
||||
margin: 0 -.3em;
|
||||
padding: .4em .5em;
|
||||
border: none;
|
||||
border-bottom: 1px solid #cdc;
|
||||
min-width: calc(100% - .6em);
|
||||
line-height: 1.1em;
|
||||
}
|
||||
pre code:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
pre code::before {
|
||||
content: counter(precode);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
font-size: .75em;
|
||||
color: #48a;
|
||||
width: 4em;
|
||||
padding-right: 1.5em;
|
||||
margin-left: -5.5em;
|
||||
}
|
||||
pre code:hover {
|
||||
background: #fec;
|
||||
color: #360;
|
||||
}
|
||||
h1, h2 {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.7em;
|
||||
text-align: center;
|
||||
border: 1em solid #777;
|
||||
border-width: .05em 0;
|
||||
margin: 3em 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
background: #f7f7f7;
|
||||
border-top: .07em solid #fff;
|
||||
border-bottom: .07em solid #bbb;
|
||||
border-radius: .5em .5em 0 0;
|
||||
padding-left: .4em;
|
||||
margin-top: 3em;
|
||||
}
|
||||
h3 {
|
||||
border-bottom: .1em solid #999;
|
||||
}
|
||||
h1 a, h3 a, h5 a,
|
||||
h2 a, h4 a, h6 a {
|
||||
color: inherit;
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#mp ul,
|
||||
#mp ol {
|
||||
border-left: .3em solid #ddd;
|
||||
}
|
||||
#m>ul,
|
||||
#m>ol {
|
||||
border-color: #bbb;
|
||||
}
|
||||
#mp ul>li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
#mp ul>li,
|
||||
#mp ol>li {
|
||||
margin: .7em 0;
|
||||
}
|
||||
strong {
|
||||
color: #000;
|
||||
}
|
||||
p>em,
|
||||
li>em,
|
||||
td>em {
|
||||
color: #c50;
|
||||
padding: .1em;
|
||||
border-bottom: .1em solid #bbb;
|
||||
}
|
||||
blockquote {
|
||||
font-family: serif;
|
||||
background: #f7f7f7;
|
||||
border: .07em dashed #ccc;
|
||||
padding: 0 2em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
small {
|
||||
opacity: .8;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
#toc {
|
||||
margin: 0 1em;
|
||||
@@ -190,14 +72,6 @@ small {
|
||||
color: #6b3;
|
||||
text-shadow: .02em 0 0 #6b3;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
}
|
||||
th, td {
|
||||
padding: .2em .5em;
|
||||
border: .12em solid #aaa;
|
||||
}
|
||||
blink {
|
||||
animation: blinker .7s cubic-bezier(.9, 0, .1, 1) infinite;
|
||||
}
|
||||
@@ -210,6 +84,36 @@ blink {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mdo pre {
|
||||
counter-reset: precode;
|
||||
}
|
||||
.mdo pre code {
|
||||
counter-increment: precode;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
border-bottom: 1px solid #cdc;
|
||||
min-width: calc(100% - .6em);
|
||||
}
|
||||
.mdo pre code:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.mdo pre code::before {
|
||||
content: counter(precode);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
font-size: .75em;
|
||||
color: #48a;
|
||||
width: 4em;
|
||||
padding-right: 1.5em;
|
||||
margin-left: -5.5em;
|
||||
}
|
||||
|
||||
|
||||
@media screen {
|
||||
html, body {
|
||||
margin: 0;
|
||||
@@ -226,34 +130,6 @@ blink {
|
||||
#mp {
|
||||
max-width: 52em;
|
||||
margin-bottom: 6em;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word; /*ie*/
|
||||
}
|
||||
a {
|
||||
color: #fff;
|
||||
background: #39b;
|
||||
text-decoration: none;
|
||||
padding: 0 .3em;
|
||||
border: none;
|
||||
border-bottom: .07em solid #079;
|
||||
}
|
||||
h2 {
|
||||
color: #fff;
|
||||
background: #555;
|
||||
margin-top: 2em;
|
||||
border-bottom: .22em solid #999;
|
||||
border-top: none;
|
||||
}
|
||||
h1 {
|
||||
color: #fff;
|
||||
background: #444;
|
||||
font-weight: normal;
|
||||
border-top: .4em solid #fb0;
|
||||
border-bottom: .4em solid #777;
|
||||
border-radius: 0 1em 0 1em;
|
||||
margin: 3em 0 1em 0;
|
||||
padding: .5em 0;
|
||||
}
|
||||
#mn {
|
||||
padding: 1.3em 0 .7em 1em;
|
||||
@@ -306,6 +182,8 @@ blink {
|
||||
color: #444;
|
||||
background: none;
|
||||
text-decoration: underline;
|
||||
margin: 0 .1em;
|
||||
padding: 0 .3em;
|
||||
border: none;
|
||||
}
|
||||
#mh a:hover {
|
||||
@@ -334,6 +212,10 @@ blink {
|
||||
#toolsbox a+a {
|
||||
text-decoration: none;
|
||||
}
|
||||
#lno {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -354,55 +236,6 @@ blink {
|
||||
html.dark #toc li {
|
||||
border-width: 0;
|
||||
}
|
||||
html.dark #mp a {
|
||||
background: #057;
|
||||
}
|
||||
html.dark #mp h1 a, html.dark #mp h4 a,
|
||||
html.dark #mp h2 a, html.dark #mp h5 a,
|
||||
html.dark #mp h3 a, html.dark #mp h6 a {
|
||||
color: inherit;
|
||||
background: none;
|
||||
}
|
||||
html.dark pre,
|
||||
html.dark code {
|
||||
color: #8c0;
|
||||
background: #1a1a1a;
|
||||
border: .07em solid #333;
|
||||
}
|
||||
html.dark #mp ul,
|
||||
html.dark #mp ol {
|
||||
border-color: #444;
|
||||
}
|
||||
html.dark #m>ul,
|
||||
html.dark #m>ol {
|
||||
border-color: #555;
|
||||
}
|
||||
html.dark strong {
|
||||
color: #fff;
|
||||
}
|
||||
html.dark p>em,
|
||||
html.dark li>em,
|
||||
html.dark td>em {
|
||||
color: #f94;
|
||||
border-color: #666;
|
||||
}
|
||||
html.dark h1 {
|
||||
background: #383838;
|
||||
border-top: .4em solid #b80;
|
||||
border-bottom: .4em solid #4c4c4c;
|
||||
}
|
||||
html.dark h2 {
|
||||
background: #444;
|
||||
border-bottom: .22em solid #555;
|
||||
}
|
||||
html.dark td,
|
||||
html.dark th {
|
||||
border-color: #444;
|
||||
}
|
||||
html.dark blockquote {
|
||||
background: #282828;
|
||||
border: .07em dashed #444;
|
||||
}
|
||||
html.dark #mn a:not(:last-child)::after {
|
||||
border-color: rgba(255,255,255,0.3);
|
||||
}
|
||||
@@ -508,7 +341,7 @@ blink {
|
||||
mso-footer-margin: .6in;
|
||||
mso-paper-source: 0;
|
||||
}
|
||||
a {
|
||||
.mdo a {
|
||||
color: #079;
|
||||
text-decoration: none;
|
||||
border-bottom: .07em solid #4ac;
|
||||
@@ -541,18 +374,20 @@ blink {
|
||||
a[ctr]::before {
|
||||
content: attr(ctr) '. ';
|
||||
}
|
||||
h1 {
|
||||
.mdo h1 {
|
||||
margin: 2em 0;
|
||||
}
|
||||
h2 {
|
||||
.mdo h2 {
|
||||
margin: 2em 0 0 0;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
.mdo h1,
|
||||
.mdo h2,
|
||||
.mdo h3 {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
h1::after,
|
||||
h2::after,
|
||||
h3::after {
|
||||
.mdo h1::after,
|
||||
.mdo h2::after,
|
||||
.mdo h3::after {
|
||||
content: 'orz';
|
||||
color: transparent;
|
||||
display: block;
|
||||
@@ -560,20 +395,20 @@ blink {
|
||||
padding: 4em 0 0 0;
|
||||
margin: 0 0 -5em 0;
|
||||
}
|
||||
p {
|
||||
.mdo p {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
table {
|
||||
.mdo table {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
tr {
|
||||
.mdo tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
thead {
|
||||
.mdo thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
tfoot {
|
||||
.mdo tfoot {
|
||||
display: table-footer-group;
|
||||
}
|
||||
#mp a.vis::after {
|
||||
@@ -581,31 +416,32 @@ blink {
|
||||
border-bottom: 1px solid #bbb;
|
||||
color: #444;
|
||||
}
|
||||
blockquote {
|
||||
.mdo blockquote {
|
||||
border-color: #555;
|
||||
}
|
||||
code {
|
||||
.mdo code {
|
||||
border-color: #bbb;
|
||||
}
|
||||
pre, pre code {
|
||||
.mdo pre,
|
||||
.mdo pre code {
|
||||
border-color: #999;
|
||||
}
|
||||
pre code::before {
|
||||
.mdo pre code::before {
|
||||
color: #058;
|
||||
}
|
||||
|
||||
|
||||
|
||||
html.dark a {
|
||||
html.dark .mdo a {
|
||||
color: #000;
|
||||
}
|
||||
html.dark pre,
|
||||
html.dark code {
|
||||
html.dark .mdo pre,
|
||||
html.dark .mdo code {
|
||||
color: #240;
|
||||
}
|
||||
html.dark p>em,
|
||||
html.dark li>em,
|
||||
html.dark td>em {
|
||||
html.dark .mdo p>em,
|
||||
html.dark .mdo li>em,
|
||||
html.dark .mdo td>em {
|
||||
color: #940;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<a id="lightswitch" href="#">go dark</a>
|
||||
<a id="navtoggle" href="#">hide nav</a>
|
||||
{%- if edit %}
|
||||
<a id="save" href="?edit" tt="Hotkey: ctrl-s">save</a>
|
||||
<a id="save" href="{{ arg_base }}edit" tt="Hotkey: ctrl-s">save</a>
|
||||
<a id="sbs" href="#" tt="editor and preview side by side">sbs</a>
|
||||
<a id="nsbs" href="#" tt="switch between editor and preview$NHotkey: ctrl-e">editor</a>
|
||||
<div id="toolsbox">
|
||||
@@ -26,10 +26,11 @@
|
||||
<a id="cfg_uni" href="#">non-ascii: whitelist</a>
|
||||
<a id="help" href="#">help</a>
|
||||
</div>
|
||||
<span id="lno">L#</span>
|
||||
{%- else %}
|
||||
<a href="?edit" tt="good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown">edit (basic)</a>
|
||||
<a href="?edit2" tt="not in-house so probably less buggy">edit (fancy)</a>
|
||||
<a href="?raw">view raw</a>
|
||||
<a href="{{ arg_base }}edit" tt="good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown">edit (basic)</a>
|
||||
<a href="{{ arg_base }}edit2" tt="not in-house so probably less buggy">edit (fancy)</a>
|
||||
<a href="{{ arg_base }}raw">view raw</a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
<div id="toc"></div>
|
||||
@@ -43,7 +44,7 @@
|
||||
if you're still reading this, check that javascript is allowed
|
||||
</div>
|
||||
</div>
|
||||
<div id="mp"></div>
|
||||
<div id="mp" class="mdo"></div>
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
|
||||
|
||||
@@ -24,23 +24,6 @@ var dbg = function () { };
|
||||
var md_plug = {};
|
||||
|
||||
|
||||
function hesc(txt) {
|
||||
return txt.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
|
||||
function cls(dom, name, add) {
|
||||
var re = new RegExp('(^| )' + name + '( |$)');
|
||||
var lst = (dom.getAttribute('class') + '').replace(re, "$1$2").replace(/ /, "");
|
||||
dom.setAttribute('class', lst + (add ? ' ' + name : ''));
|
||||
}
|
||||
|
||||
|
||||
function statify(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
|
||||
// dodge browser issues
|
||||
(function () {
|
||||
var ua = navigator.userAgent;
|
||||
@@ -65,7 +48,7 @@ function statify(obj) {
|
||||
if (a > 0)
|
||||
loc.push(n[a]);
|
||||
|
||||
var dec = hesc(uricom_dec(n[a])[0]);
|
||||
var dec = esc(uricom_dec(n[a])[0]);
|
||||
|
||||
nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
|
||||
}
|
||||
@@ -73,6 +56,26 @@ function statify(obj) {
|
||||
})();
|
||||
|
||||
|
||||
// image load handler
|
||||
var img_load = (function () {
|
||||
var r = {};
|
||||
r.callbacks = [];
|
||||
|
||||
function fire() {
|
||||
for (var a = 0; a < r.callbacks.length; a++)
|
||||
r.callbacks[a]();
|
||||
}
|
||||
|
||||
var timeout = null;
|
||||
r.done = function () {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(fire, 500);
|
||||
};
|
||||
|
||||
return r;
|
||||
})();
|
||||
|
||||
|
||||
// faster than replacing the entire html (chrome 1.8x, firefox 1.6x)
|
||||
function copydom(src, dst, lv) {
|
||||
var sc = src.childNodes,
|
||||
@@ -264,7 +267,14 @@ function convert_markdown(md_text, dest_dom) {
|
||||
|
||||
throw ex;
|
||||
}
|
||||
var md_dom = new DOMParser().parseFromString(md_html, "text/html").body;
|
||||
var md_dom = dest_dom;
|
||||
try {
|
||||
md_dom = new DOMParser().parseFromString(md_html, "text/html").body;
|
||||
}
|
||||
catch (ex) {
|
||||
md_dom.innerHTML = md_html;
|
||||
window.copydom = noop;
|
||||
}
|
||||
|
||||
var nodes = md_dom.getElementsByTagName('a');
|
||||
for (var a = nodes.length - 1; a >= 0; a--) {
|
||||
@@ -356,6 +366,10 @@ function convert_markdown(md_text, dest_dom) {
|
||||
|
||||
copydom(md_dom, dest_dom, 0);
|
||||
|
||||
var imgs = dest_dom.getElementsByTagName('img');
|
||||
for (var a = 0, aa = imgs.length; a < aa; a++)
|
||||
imgs[a].onload = img_load.done;
|
||||
|
||||
if (ext && ext[0].render2)
|
||||
try {
|
||||
ext[0].render2(dest_dom);
|
||||
@@ -490,13 +504,16 @@ function init_toc() {
|
||||
// "main" :p
|
||||
convert_markdown(dom_src.value, dom_pre);
|
||||
var toc = init_toc();
|
||||
img_load.callbacks = [toc.refresh];
|
||||
|
||||
|
||||
// scroll handler
|
||||
var redraw = (function () {
|
||||
var sbs = false;
|
||||
var sbs = true;
|
||||
function onresize() {
|
||||
sbs = window.matchMedia('(min-width: 64em)').matches;
|
||||
if (window.matchMedia)
|
||||
sbs = window.matchMedia('(min-width: 64em)').matches;
|
||||
|
||||
var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';
|
||||
if (sbs) {
|
||||
dom_toc.style.top = y;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'consolas', monospace, monospace;
|
||||
font-family: 'scp', monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@@ -98,7 +98,7 @@ var draw_md = (function () {
|
||||
var src = dom_src.value;
|
||||
convert_markdown(src, dom_pre);
|
||||
|
||||
var lines = hesc(src).replace(/\r/g, "").split('\n');
|
||||
var lines = esc(src).replace(/\r/g, "").split('\n');
|
||||
nlines = lines.length;
|
||||
var html = [];
|
||||
for (var a = 0; a < lines.length; a++)
|
||||
@@ -108,7 +108,7 @@ var draw_md = (function () {
|
||||
map_src = genmap(dom_ref, map_src);
|
||||
map_pre = genmap(dom_pre, map_pre);
|
||||
|
||||
cls(ebi('save'), 'disabled', src == server_md);
|
||||
clmod(ebi('save'), 'disabled', src == server_md);
|
||||
|
||||
var t1 = Date.now();
|
||||
delay = t1 - t0 > 100 ? 25 : 1;
|
||||
@@ -127,6 +127,12 @@ var draw_md = (function () {
|
||||
})();
|
||||
|
||||
|
||||
// discard TOC callback, just regen editor scroll map
|
||||
img_load.callbacks = [function () {
|
||||
map_pre = genmap(dom_pre, map_pre);
|
||||
}];
|
||||
|
||||
|
||||
// resize handler
|
||||
redraw = (function () {
|
||||
function onresize() {
|
||||
@@ -136,7 +142,6 @@ redraw = (function () {
|
||||
dom_ref.style.width = getComputedStyle(dom_src).offsetWidth + 'px';
|
||||
map_src = genmap(dom_ref, map_src);
|
||||
map_pre = genmap(dom_pre, map_pre);
|
||||
dbg(document.body.clientWidth + 'x' + document.body.clientHeight);
|
||||
}
|
||||
function setsbs() {
|
||||
dom_wrap.setAttribute('class', '');
|
||||
@@ -225,44 +230,40 @@ redraw = (function () {
|
||||
|
||||
// modification checker
|
||||
function Modpoll() {
|
||||
this.skip_one = true;
|
||||
this.disabled = false;
|
||||
|
||||
this.periodic = function () {
|
||||
var that = this;
|
||||
setTimeout(function () {
|
||||
that.periodic();
|
||||
}, 1000 * md_opt.modpoll_freq);
|
||||
var r = {
|
||||
skip_one: true,
|
||||
disabled: false
|
||||
};
|
||||
|
||||
r.periodic = function () {
|
||||
var skip = null;
|
||||
|
||||
if (toast.visible)
|
||||
skip = 'toast';
|
||||
|
||||
else if (this.skip_one)
|
||||
else if (r.skip_one)
|
||||
skip = 'saved';
|
||||
|
||||
else if (this.disabled)
|
||||
else if (r.disabled)
|
||||
skip = 'disabled';
|
||||
|
||||
if (skip) {
|
||||
console.log('modpoll skip, ' + skip);
|
||||
this.skip_one = false;
|
||||
r.skip_one = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('modpoll...');
|
||||
var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now();
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.modpoll = this;
|
||||
xhr.open('GET', url, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.onreadystatechange = this.cb;
|
||||
xhr.onreadystatechange = r.cb;
|
||||
xhr.send();
|
||||
}
|
||||
};
|
||||
|
||||
this.cb = function () {
|
||||
if (this.modpoll.disabled || this.modpoll.skip_one) {
|
||||
r.cb = function () {
|
||||
if (r.disabled || r.skip_one) {
|
||||
console.log('modpoll abort');
|
||||
return;
|
||||
}
|
||||
@@ -283,7 +284,7 @@ function Modpoll() {
|
||||
|
||||
if (server_ref != server_now) {
|
||||
console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|");
|
||||
this.modpoll.disabled = true;
|
||||
r.disabled = true;
|
||||
var msg = [
|
||||
"The document has changed on the server.",
|
||||
"The changes will NOT be loaded into your editor automatically.",
|
||||
@@ -297,12 +298,12 @@ function Modpoll() {
|
||||
}
|
||||
|
||||
console.log('modpoll eq');
|
||||
}
|
||||
};
|
||||
|
||||
if (md_opt.modpoll_freq > 0)
|
||||
this.periodic();
|
||||
setInterval(r.periodic, 1000 * md_opt.modpoll_freq);
|
||||
|
||||
return this;
|
||||
return r;
|
||||
}
|
||||
var modpoll = new Modpoll();
|
||||
|
||||
@@ -874,6 +875,40 @@ function cfg_uni(e) {
|
||||
}
|
||||
|
||||
|
||||
var set_lno = (function () {
|
||||
var t = null,
|
||||
pi = null,
|
||||
pv = null,
|
||||
lno = ebi('lno');
|
||||
|
||||
function poke() {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(fire, 20);
|
||||
}
|
||||
|
||||
function fire() {
|
||||
try {
|
||||
clearTimeout(t);
|
||||
|
||||
var i = dom_src.selectionStart;
|
||||
if (i === pi)
|
||||
return;
|
||||
|
||||
var v = 'L' + dom_src.value.slice(0, i).split('\n').length;
|
||||
if (v != pv)
|
||||
lno.innerHTML = v;
|
||||
|
||||
pi = i;
|
||||
pv = v;
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
|
||||
timer.add(fire);
|
||||
return poke;
|
||||
})();
|
||||
|
||||
|
||||
// hotkeys / toolbar
|
||||
(function () {
|
||||
function keydown(ev) {
|
||||
@@ -892,6 +927,8 @@ function cfg_uni(e) {
|
||||
if (document.activeElement != dom_src)
|
||||
return true;
|
||||
|
||||
set_lno();
|
||||
|
||||
if (ctrl(ev)) {
|
||||
if (ev.code == "KeyH" || kc == 72) {
|
||||
md_header(ev.shiftKey);
|
||||
@@ -1086,9 +1123,9 @@ action_stack = (function () {
|
||||
ref = newtxt;
|
||||
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
|
||||
if (hist.un.length > 0)
|
||||
dbg(statify(hist.un.slice(-1)[0]));
|
||||
dbg(jcp(hist.un.slice(-1)[0]));
|
||||
if (hist.re.length > 0)
|
||||
dbg(statify(hist.re.slice(-1)[0]));
|
||||
dbg(jcp(hist.re.slice(-1)[0]));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,8 @@ html .editor-toolbar>button.active { border-color: rgba(0,0,0,0.4); background:
|
||||
html .editor-toolbar>i.separator { border-left: 1px solid #ccc; }
|
||||
html .editor-toolbar.disabled-for-preview>button:not(.no-disable) { opacity: .35 }
|
||||
|
||||
|
||||
|
||||
html {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
@@ -31,6 +33,9 @@ html, body {
|
||||
background: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#mn {
|
||||
font-weight: normal;
|
||||
margin: 1.3em 0 .7em 1em;
|
||||
@@ -72,148 +77,12 @@ html .editor-toolbar>button.disabled {
|
||||
html .editor-toolbar>button.save.force-save {
|
||||
background: #f97;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* copied from md.css for now */
|
||||
.mdo pre,
|
||||
.mdo code,
|
||||
.mdo a {
|
||||
color: #480;
|
||||
background: #f7f7f7;
|
||||
border: .07em solid #ddd;
|
||||
border-radius: .2em;
|
||||
padding: .1em .3em;
|
||||
margin: 0 .1em;
|
||||
}
|
||||
.mdo code {
|
||||
font-size: .96em;
|
||||
}
|
||||
.mdo pre,
|
||||
.mdo code {
|
||||
font-family: monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.mdo pre code {
|
||||
display: block;
|
||||
margin: 0 -.3em;
|
||||
padding: .4em .5em;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
.mdo a {
|
||||
color: #fff;
|
||||
background: #39b;
|
||||
text-decoration: none;
|
||||
padding: 0 .3em;
|
||||
border: none;
|
||||
border-bottom: .07em solid #079;
|
||||
}
|
||||
.mdo h2 {
|
||||
color: #fff;
|
||||
background: #555;
|
||||
margin-top: 2em;
|
||||
border-bottom: .22em solid #999;
|
||||
border-top: none;
|
||||
}
|
||||
.mdo h1 {
|
||||
color: #fff;
|
||||
background: #444;
|
||||
font-weight: normal;
|
||||
border-top: .4em solid #fb0;
|
||||
border-bottom: .4em solid #777;
|
||||
border-radius: 0 1em 0 1em;
|
||||
margin: 3em 0 1em 0;
|
||||
padding: .5em 0;
|
||||
}
|
||||
h1, h2 {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.7em;
|
||||
text-align: center;
|
||||
border: 1em solid #777;
|
||||
border-width: .05em 0;
|
||||
margin: 3em 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
background: #f7f7f7;
|
||||
border-top: .07em solid #fff;
|
||||
border-bottom: .07em solid #bbb;
|
||||
border-radius: .5em .5em 0 0;
|
||||
padding-left: .4em;
|
||||
margin-top: 3em;
|
||||
}
|
||||
.mdo ul,
|
||||
.mdo ol {
|
||||
border-left: .3em solid #ddd;
|
||||
}
|
||||
.mdo>ul,
|
||||
.mdo>ol {
|
||||
border-color: #bbb;
|
||||
}
|
||||
.mdo ul>li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.mdo ul>li,
|
||||
.mdo ol>li {
|
||||
margin: .7em 0;
|
||||
}
|
||||
strong {
|
||||
color: #000;
|
||||
}
|
||||
p>em,
|
||||
li>em,
|
||||
td>em {
|
||||
color: #c50;
|
||||
padding: .1em;
|
||||
border-bottom: .1em solid #bbb;
|
||||
}
|
||||
blockquote {
|
||||
font-family: serif;
|
||||
background: #f7f7f7;
|
||||
border: .07em dashed #ccc;
|
||||
padding: 0 2em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
small {
|
||||
opacity: .8;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
td {
|
||||
padding: .2em .5em;
|
||||
border: .12em solid #aaa;
|
||||
}
|
||||
th {
|
||||
border: .12em solid #aaa;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* mde support */
|
||||
.mdo {
|
||||
padding: 1em;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
html.dark .mdo {
|
||||
background: #1c1c1c;
|
||||
}
|
||||
.CodeMirror {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* darkmode */
|
||||
html.dark .mdo,
|
||||
html.dark .CodeMirror {
|
||||
@@ -237,55 +106,6 @@ html.dark .CodeMirror-selectedtext {
|
||||
background: #246;
|
||||
color: #fff;
|
||||
}
|
||||
html.dark .mdo a {
|
||||
background: #057;
|
||||
}
|
||||
html.dark .mdo h1 a, html.dark .mdo h4 a,
|
||||
html.dark .mdo h2 a, html.dark .mdo h5 a,
|
||||
html.dark .mdo h3 a, html.dark .mdo h6 a {
|
||||
color: inherit;
|
||||
background: none;
|
||||
}
|
||||
html.dark pre,
|
||||
html.dark code {
|
||||
color: #8c0;
|
||||
background: #1a1a1a;
|
||||
border: .07em solid #333;
|
||||
}
|
||||
html.dark .mdo ul,
|
||||
html.dark .mdo ol {
|
||||
border-color: #444;
|
||||
}
|
||||
html.dark .mdo>ul,
|
||||
html.dark .mdo>ol {
|
||||
border-color: #555;
|
||||
}
|
||||
html.dark strong {
|
||||
color: #fff;
|
||||
}
|
||||
html.dark p>em,
|
||||
html.dark li>em,
|
||||
html.dark td>em {
|
||||
color: #f94;
|
||||
border-color: #666;
|
||||
}
|
||||
html.dark h1 {
|
||||
background: #383838;
|
||||
border-top: .4em solid #b80;
|
||||
border-bottom: .4em solid #4c4c4c;
|
||||
}
|
||||
html.dark h2 {
|
||||
background: #444;
|
||||
border-bottom: .22em solid #555;
|
||||
}
|
||||
html.dark td,
|
||||
html.dark th {
|
||||
border-color: #444;
|
||||
}
|
||||
html.dark blockquote {
|
||||
background: #282828;
|
||||
border: .07em dashed #444;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -321,4 +141,15 @@ html.dark .editor-toolbar>button.active {
|
||||
html.dark .editor-toolbar::after,
|
||||
html.dark .editor-toolbar::before {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ui.css overrides */
|
||||
.mdo {
|
||||
padding: 1em;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
html.dark .mdo {
|
||||
background: #1c1c1c;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ l.setItem('lightmode', drk? 0:1);
|
||||
|
||||
</script>
|
||||
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||
<script src="/.cpr/deps/marked.js?_={{ ts }}"></script>
|
||||
<script src="/.cpr/deps/easymde.js?_={{ ts }}"></script>
|
||||
<script src="/.cpr/mde.js?_={{ ts }}"></script>
|
||||
</body></html>
|
||||
|
||||
@@ -55,6 +55,16 @@ table {
|
||||
.btns {
|
||||
margin: 1em 0;
|
||||
}
|
||||
#msg {
|
||||
margin: 3em 0;
|
||||
}
|
||||
#msg h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#msg h1 + p {
|
||||
margin-top: .3em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
html.dark,
|
||||
@@ -73,8 +83,8 @@ html.dark a {
|
||||
}
|
||||
html.dark input {
|
||||
color: #fff;
|
||||
background: #624;
|
||||
border: 1px solid #c27;
|
||||
background: #626;
|
||||
border: 1px solid #c2c;
|
||||
border-width: 1px 0 0 0;
|
||||
border-radius: .5em;
|
||||
padding: .5em .7em;
|
||||
|
||||
@@ -12,7 +12,17 @@
|
||||
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<p>hello {{ this.uname }}</p>
|
||||
{%- if this.uname == '*' %}
|
||||
<p>howdy stranger <small>(you're not logged in)</small></p>
|
||||
{%- else %}
|
||||
<p>welcome back, <strong>{{ this.uname }}</strong></p>
|
||||
{%- endif %}
|
||||
|
||||
{%- if msg %}
|
||||
<div id="msg">
|
||||
{{ msg }}
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
{%- if avol %}
|
||||
<h1>admin panel:</h1>
|
||||
@@ -60,7 +70,7 @@
|
||||
|
||||
<h1>login for more:</h1>
|
||||
<ul>
|
||||
<form method="post" enctype="multipart/form-data" action="/">
|
||||
<form method="post" enctype="multipart/form-data" action="/{{ qvpath }}">
|
||||
<input type="hidden" name="act" value="login" />
|
||||
<input type="password" name="cppwd" />
|
||||
<input type="submit" value="Login" />
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
@font-face {
|
||||
font-family: 'scp';
|
||||
font-display: swap;
|
||||
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(/.cpr/deps/scp.woff2) format('woff2');
|
||||
}
|
||||
html {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
@@ -52,7 +57,7 @@ html {
|
||||
opacity: 0;
|
||||
padding: .3em 0;
|
||||
margin: -.3em 0 0 0;
|
||||
line-height: 1.5em;
|
||||
line-height: 1.3em;
|
||||
color: #000;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -60,6 +65,15 @@ html {
|
||||
border-radius: .5em 0 0 .5em;
|
||||
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||
}
|
||||
#toastb {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#toast.scroll #toastb {
|
||||
overflow-y: scroll;
|
||||
margin-right: -1.2em;
|
||||
padding-right: .7em;
|
||||
}
|
||||
#toast pre {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -230,3 +244,211 @@ html.light #tt em {
|
||||
#modali:focus {
|
||||
border-color: #06d;
|
||||
}
|
||||
#repl_pre {
|
||||
max-width: 24em;
|
||||
}
|
||||
*:focus,
|
||||
#pctl *:focus,
|
||||
.btn:focus {
|
||||
box-shadow: 0 .1em .2em #fc0 inset;
|
||||
border-radius: .2em;
|
||||
}
|
||||
html.light *:focus,
|
||||
html.light #pctl *:focus,
|
||||
html.light .btn:focus {
|
||||
box-shadow: 0 .1em .2em #037 inset;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.mdo pre,
|
||||
.mdo code,
|
||||
.mdo a {
|
||||
color: #480;
|
||||
background: #f7f7f7;
|
||||
border: .07em solid #ddd;
|
||||
border-radius: .2em;
|
||||
padding: .1em .3em;
|
||||
margin: 0 .1em;
|
||||
}
|
||||
.mdo pre,
|
||||
.mdo code,
|
||||
.mdo tt {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.mdo code {
|
||||
font-size: .96em;
|
||||
}
|
||||
.mdo h1,
|
||||
.mdo h2 {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.mdo h1 {
|
||||
font-size: 1.7em;
|
||||
text-align: center;
|
||||
border: 1em solid #777;
|
||||
border-width: .05em 0;
|
||||
margin: 3em 0;
|
||||
}
|
||||
.mdo h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
background: #f7f7f7;
|
||||
border-top: .07em solid #fff;
|
||||
border-bottom: .07em solid #bbb;
|
||||
border-radius: .5em .5em 0 0;
|
||||
padding-left: .4em;
|
||||
margin-top: 3em;
|
||||
}
|
||||
.mdo h3 {
|
||||
border-bottom: .1em solid #999;
|
||||
}
|
||||
.mdo h1 a, .mdo h3 a, .mdo h5 a,
|
||||
.mdo h2 a, .mdo h4 a, .mdo h6 a {
|
||||
color: inherit;
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.mdo ul,
|
||||
.mdo ol {
|
||||
border-left: .3em solid #ddd;
|
||||
}
|
||||
.mdo ul>li,
|
||||
.mdo ol>li {
|
||||
margin: .7em 0;
|
||||
list-style-type: disc;
|
||||
}
|
||||
.mdo strong {
|
||||
color: #000;
|
||||
}
|
||||
.mdo p>em,
|
||||
.mdo li>em,
|
||||
.mdo td>em {
|
||||
color: #c50;
|
||||
padding: .1em;
|
||||
border-bottom: .1em solid #bbb;
|
||||
}
|
||||
.mdo blockquote {
|
||||
font-family: serif;
|
||||
background: #f7f7f7;
|
||||
border: .07em dashed #ccc;
|
||||
padding: 0 2em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.mdo small {
|
||||
opacity: .8;
|
||||
}
|
||||
.mdo pre code {
|
||||
display: block;
|
||||
margin: 0 -.3em;
|
||||
padding: .4em .5em;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
.mdo pre code:hover {
|
||||
background: #fec;
|
||||
color: #360;
|
||||
}
|
||||
.mdo table {
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.mdo th,
|
||||
.mdo td {
|
||||
padding: .2em .5em;
|
||||
border: .12em solid #aaa;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.mdo {
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word; /*ie*/
|
||||
}
|
||||
html.light .mdo a,
|
||||
.mdo a {
|
||||
color: #fff;
|
||||
background: #39b;
|
||||
text-decoration: none;
|
||||
padding: 0 .3em;
|
||||
border: none;
|
||||
border-bottom: .07em solid #079;
|
||||
}
|
||||
.mdo h1 {
|
||||
color: #fff;
|
||||
background: #444;
|
||||
font-weight: normal;
|
||||
border-top: .4em solid #fb0;
|
||||
border-bottom: .4em solid #777;
|
||||
border-radius: 0 1em 0 1em;
|
||||
margin: 3em 0 1em 0;
|
||||
padding: .5em 0;
|
||||
}
|
||||
.mdo h2 {
|
||||
color: #fff;
|
||||
background: #555;
|
||||
margin-top: 2em;
|
||||
border-bottom: .22em solid #999;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
html.dark .mdo a {
|
||||
background: #057;
|
||||
}
|
||||
html.dark .mdo h1 a, html.dark .mdo h4 a,
|
||||
html.dark .mdo h2 a, html.dark .mdo h5 a,
|
||||
html.dark .mdo h3 a, html.dark .mdo h6 a {
|
||||
color: inherit;
|
||||
background: none;
|
||||
}
|
||||
html.dark .mdo pre,
|
||||
html.dark .mdo code {
|
||||
color: #8c0;
|
||||
background: #1a1a1a;
|
||||
border: .07em solid #333;
|
||||
}
|
||||
html.dark .mdo ul,
|
||||
html.dark .mdo ol {
|
||||
border-color: #444;
|
||||
}
|
||||
html.dark .mdo strong {
|
||||
color: #fff;
|
||||
}
|
||||
html.dark .mdo p>em,
|
||||
html.dark .mdo li>em,
|
||||
html.dark .mdo td>em {
|
||||
color: #f94;
|
||||
border-color: #666;
|
||||
}
|
||||
html.dark .mdo h1 {
|
||||
background: #383838;
|
||||
border-top: .4em solid #b80;
|
||||
border-bottom: .4em solid #4c4c4c;
|
||||
}
|
||||
html.dark .mdo h2 {
|
||||
background: #444;
|
||||
border-bottom: .22em solid #555;
|
||||
}
|
||||
html.dark .mdo td,
|
||||
html.dark .mdo th {
|
||||
border-color: #444;
|
||||
}
|
||||
html.dark .mdo blockquote {
|
||||
background: #282828;
|
||||
border: .07em dashed #444;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +528,7 @@ function up2k_init(subtle) {
|
||||
got_deps = true;
|
||||
}
|
||||
|
||||
if (perms.length && !has(perms, 'read'))
|
||||
if (perms.length && !has(perms, 'read') && has(perms, 'write'))
|
||||
goto('up2k');
|
||||
|
||||
function setmsg(msg, type) {
|
||||
@@ -572,15 +572,17 @@ function up2k_init(subtle) {
|
||||
}
|
||||
|
||||
var parallel_uploads = icfg_get('nthread'),
|
||||
multitask = bcfg_get('multitask', true),
|
||||
ask_up = bcfg_get('ask_up', true),
|
||||
flag_en = bcfg_get('flag_en', false),
|
||||
fsearch = bcfg_get('fsearch', false),
|
||||
turbo = bcfg_get('u2turbo', false),
|
||||
datechk = bcfg_get('u2tdate', true),
|
||||
uc = {},
|
||||
fdom_ctr = 0,
|
||||
min_filebuf = 0;
|
||||
|
||||
bcfg_bind(uc, 'multitask', 'multitask', true, null, false);
|
||||
bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false);
|
||||
bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg, false);
|
||||
bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false);
|
||||
bcfg_bind(uc, 'turbo', 'u2turbo', false, draw_turbo, false);
|
||||
bcfg_bind(uc, 'datechk', 'u2tdate', true, null, false);
|
||||
|
||||
var st = {
|
||||
"files": [],
|
||||
"todo": {
|
||||
@@ -633,20 +635,97 @@ function up2k_init(subtle) {
|
||||
function nav() {
|
||||
ebi('file' + fdom_ctr).click();
|
||||
}
|
||||
ebi('u2btn').addEventListener('click', nav, false);
|
||||
ebi('u2btn').onclick = nav;
|
||||
|
||||
var nenters = 0;
|
||||
function ondrag(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (++nenters <= 0)
|
||||
nenters = 1;
|
||||
|
||||
//console.log(nenters, Date.now(), 'enter', this, e.target);
|
||||
if (onover.bind(this)(e))
|
||||
return true;
|
||||
|
||||
var mup, up = QS('#up_zd');
|
||||
var msr, sr = QS('#srch_zd');
|
||||
if (!has(perms, 'write'))
|
||||
mup = 'you do not have write-access to this folder';
|
||||
if (!has(perms, 'read'))
|
||||
msr = 'you do not have read-access to this folder';
|
||||
if (!have_up2k_idx)
|
||||
msr = 'file-search is not enabled in server config';
|
||||
|
||||
up.querySelector('span').textContent = mup || 'drop it here';
|
||||
sr.querySelector('span').textContent = msr || 'drop it here';
|
||||
clmod(up, 'err', mup);
|
||||
clmod(sr, 'err', msr);
|
||||
clmod(up, 'ok', !mup);
|
||||
clmod(sr, 'ok', !msr);
|
||||
ebi('up_dz').setAttribute('err', mup || '');
|
||||
ebi('srch_dz').setAttribute('err', msr || '');
|
||||
}
|
||||
function onover(e) {
|
||||
try {
|
||||
var ok = false, dt = e.dataTransfer.types;
|
||||
for (var a = 0; a < dt.length; a++)
|
||||
if (dt[a] == 'Files')
|
||||
ok = true;
|
||||
|
||||
if (!ok)
|
||||
return true;
|
||||
}
|
||||
catch (ex) { }
|
||||
|
||||
ev(e);
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
clmod(ebi('drops'), 'vis', 1);
|
||||
var v = this.getAttribute('v');
|
||||
if (v)
|
||||
clmod(ebi(v), 'hl', 1);
|
||||
}
|
||||
ebi('u2btn').addEventListener('dragover', ondrag, false);
|
||||
ebi('u2btn').addEventListener('dragenter', ondrag, false);
|
||||
function offdrag(e) {
|
||||
ev(e);
|
||||
|
||||
var v = this.getAttribute('v');
|
||||
if (v)
|
||||
clmod(ebi(v), 'hl');
|
||||
|
||||
if (--nenters <= 0) {
|
||||
clmod(ebi('drops'), 'vis');
|
||||
clmod(ebi('up_dz'), 'hl');
|
||||
clmod(ebi('srch_dz'), 'hl');
|
||||
}
|
||||
|
||||
//console.log(nenters, Date.now(), 'leave', this, e && e.target);
|
||||
}
|
||||
document.body.ondragenter = ondrag;
|
||||
document.body.ondragleave = offdrag;
|
||||
|
||||
var drops = [ebi('up_dz'), ebi('srch_dz')];
|
||||
for (var a = 0; a < 2; a++) {
|
||||
drops[a].ondragenter = ondrag;
|
||||
drops[a].ondragover = onover;
|
||||
drops[a].ondragleave = offdrag;
|
||||
drops[a].ondrop = gotfile;
|
||||
}
|
||||
ebi('drops').onclick = offdrag; // old ff
|
||||
|
||||
function gotfile(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
ev(e);
|
||||
nenters = 0;
|
||||
offdrag.bind(this)();
|
||||
var dz = (this && this.getAttribute('id'));
|
||||
|
||||
var err = this.getAttribute('err');
|
||||
if (err)
|
||||
return modal.alert('sorry, ' + err);
|
||||
|
||||
if ((dz == 'up_dz' && uc.fsearch) || (dz == 'srch_dz' && !uc.fsearch))
|
||||
tgl_fsearch();
|
||||
|
||||
if (!QS('#op_up2k.act'))
|
||||
goto('up2k');
|
||||
|
||||
var files,
|
||||
is_itemlist = false;
|
||||
@@ -806,11 +885,11 @@ function up2k_init(subtle) {
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
});
|
||||
|
||||
var msg = ['{0} these {1} files?<ul>'.format(fsearch ? 'search' : 'upload', good_files.length)];
|
||||
var msg = ['{0} these {1} files?<ul>'.format(uc.fsearch ? 'search' : 'upload', good_files.length)];
|
||||
for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++)
|
||||
msg.push('<li>' + esc(good_files[a][1]) + '</li>');
|
||||
|
||||
if (ask_up && !fsearch)
|
||||
if (uc.ask_up && !uc.fsearch)
|
||||
return modal.confirm(msg.join('') + '</ul>', function () { up_them(good_files); }, null);
|
||||
|
||||
up_them(good_files);
|
||||
@@ -850,7 +929,7 @@ function up2k_init(subtle) {
|
||||
},
|
||||
key = entry.name + '\n' + entry.size;
|
||||
|
||||
if (fsearch)
|
||||
if (uc.fsearch)
|
||||
entry.srch = 1;
|
||||
|
||||
if (seen[key])
|
||||
@@ -859,7 +938,7 @@ function up2k_init(subtle) {
|
||||
seen[key] = 1;
|
||||
|
||||
pvis.addfile([
|
||||
fsearch ? esc(entry.name) : linksplit(
|
||||
uc.fsearch ? esc(entry.name) : linksplit(
|
||||
uricom_dec(entry.purl)[0] + entry.name).join(' '),
|
||||
'📐 hash',
|
||||
''
|
||||
@@ -867,7 +946,7 @@ function up2k_init(subtle) {
|
||||
|
||||
st.bytes.total += fobj.size;
|
||||
st.files.push(entry);
|
||||
if (turbo)
|
||||
if (uc.turbo)
|
||||
push_t(st.todo.head, entry);
|
||||
else
|
||||
push_t(st.todo.hash, entry);
|
||||
@@ -877,14 +956,13 @@ function up2k_init(subtle) {
|
||||
pvis.changecard(pvis.act);
|
||||
}
|
||||
}
|
||||
ebi('u2btn').addEventListener('drop', gotfile, false);
|
||||
|
||||
function more_one_file() {
|
||||
fdom_ctr++;
|
||||
var elm = mknod('div');
|
||||
elm.innerHTML = '<input id="file{0}" type="file" name="file{0}[]" multiple="multiple" />'.format(fdom_ctr);
|
||||
ebi('u2form').appendChild(elm);
|
||||
ebi('file' + fdom_ctr).addEventListener('change', gotfile, false);
|
||||
ebi('file' + fdom_ctr).onchange = gotfile;
|
||||
}
|
||||
more_one_file();
|
||||
|
||||
@@ -904,7 +982,7 @@ function up2k_init(subtle) {
|
||||
}
|
||||
ebi('u2cleanup').onclick = u2cleanup;
|
||||
|
||||
var etaref = 0, etaskip = 0;
|
||||
var etaref = 0, etaskip = 0, op_minh = 0;
|
||||
function etafun() {
|
||||
var nhash = st.busy.head.length + st.busy.hash.length + st.todo.head.length + st.todo.hash.length,
|
||||
nsend = st.busy.upload.length + st.todo.upload.length,
|
||||
@@ -912,8 +990,20 @@ function up2k_init(subtle) {
|
||||
td = (now - (etaref || now)) / 1000.0;
|
||||
|
||||
etaref = now;
|
||||
if (td > 1.2)
|
||||
td = 0.05;
|
||||
|
||||
//ebi('acc_info').innerHTML = humantime(st.time.busy) + ' ' + f2f(now / 1000, 1);
|
||||
|
||||
var op = ebi('op_up2k'),
|
||||
uff = ebi('u2footfoot'),
|
||||
minh = QS('#op_up2k.act') ? Math.max(op_minh, uff.offsetTop + uff.offsetHeight - op.offsetTop + 32) : 0;
|
||||
|
||||
if (minh > op_minh || !op_minh) {
|
||||
op_minh = minh;
|
||||
op.style.minHeight = op_minh + 'px';
|
||||
}
|
||||
|
||||
if (!nhash)
|
||||
ebi('u2etah').innerHTML = 'Done ({0}, {1} files)'.format(humansize(st.bytes.hashed), pvis.ctr["ok"] + pvis.ctr["ng"]);
|
||||
|
||||
@@ -931,14 +1021,14 @@ function up2k_init(subtle) {
|
||||
if (nhash) {
|
||||
st.time.hashing += td;
|
||||
t.push(['u2etah', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
|
||||
if (fsearch)
|
||||
if (uc.fsearch)
|
||||
t.push(['u2etat', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
|
||||
}
|
||||
if (nsend) {
|
||||
st.time.uploading += td;
|
||||
t.push(['u2etau', st.bytes.uploaded, st.bytes.finished, st.time.uploading]);
|
||||
}
|
||||
if ((nhash || nsend) && !fsearch) {
|
||||
if ((nhash || nsend) && !uc.fsearch) {
|
||||
if (!st.bytes.finished) {
|
||||
ebi('u2etat').innerHTML = '(preparing to upload)';
|
||||
}
|
||||
@@ -996,7 +1086,7 @@ function up2k_init(subtle) {
|
||||
if (st.files[n].t_uploading)
|
||||
return false;
|
||||
|
||||
if ((multitask ? 1 : 0) <
|
||||
if ((uc.multitask ? 1 : 0) <
|
||||
st.todo.upload.length +
|
||||
st.busy.upload.length)
|
||||
return false;
|
||||
@@ -1008,7 +1098,7 @@ function up2k_init(subtle) {
|
||||
if (!parallel_uploads)
|
||||
return false;
|
||||
|
||||
if (multitask) {
|
||||
if (uc.multitask) {
|
||||
var ahead = st.bytes.hashed - st.bytes.finished;
|
||||
return ahead < 1024 * 1024 * 1024 * 4 &&
|
||||
st.todo.handshake.length + st.busy.handshake.length < 16;
|
||||
@@ -1055,13 +1145,13 @@ function up2k_init(subtle) {
|
||||
"EventListener"]("beforeunload", warn_uploader_busy);
|
||||
|
||||
if (!is_busy) {
|
||||
var k = fsearch ? 'searches' : 'uploads',
|
||||
ks = fsearch ? 'Search' : 'Upload',
|
||||
tok = fsearch ? 'successful (found on server)' : 'completed successfully',
|
||||
tng = fsearch ? 'failed (NOT found on server)' : 'failed, sorry',
|
||||
var k = uc.fsearch ? 'searches' : 'uploads',
|
||||
ks = uc.fsearch ? 'Search' : 'Upload',
|
||||
tok = uc.fsearch ? 'successful (found on server)' : 'completed successfully',
|
||||
tng = uc.fsearch ? 'failed (NOT found on server)' : 'failed, sorry',
|
||||
ok = pvis.ctr["ok"],
|
||||
ng = pvis.ctr["ng"],
|
||||
t = ask_up ? 0 : 10;
|
||||
t = uc.ask_up ? 0 : 10;
|
||||
|
||||
if (ok && ng)
|
||||
toast.warn(t, 'Finished, but some {0} failed:\n{1} {2},\n{3} {4}'.format(k, ok, tok, ng, tng));
|
||||
@@ -1075,6 +1165,7 @@ function up2k_init(subtle) {
|
||||
toast.err(t, '{0} {1}'.format(ks, tng));
|
||||
|
||||
timer.rm(etafun);
|
||||
op_minh = 0;
|
||||
}
|
||||
else {
|
||||
timer.add(etafun, false);
|
||||
@@ -1364,14 +1455,17 @@ function up2k_init(subtle) {
|
||||
srv_ts = xhr.getResponseHeader('Last-Modified');
|
||||
|
||||
ok = t.size == srv_sz;
|
||||
if (ok && datechk) {
|
||||
if (ok && uc.datechk) {
|
||||
srv_ts = new Date(srv_ts) / 1000;
|
||||
ok = Math.abs(srv_ts - t.lmod) < 2;
|
||||
}
|
||||
}
|
||||
apop(st.busy.head, t);
|
||||
if (!ok && !t.srch)
|
||||
return push_t(st.todo.hash, t);
|
||||
if (!ok && !t.srch) {
|
||||
push_t(st.todo.hash, t);
|
||||
tasker();
|
||||
return;
|
||||
}
|
||||
|
||||
t.done = true;
|
||||
st.bytes.hashed += t.size;
|
||||
@@ -1380,6 +1474,7 @@ function up2k_init(subtle) {
|
||||
pvis.seth(t.n, 1, ok ? 'YOLO' : '404');
|
||||
pvis.seth(t.n, 2, "turbo'd");
|
||||
pvis.move(t.n, ok ? 'ok' : 'ng');
|
||||
tasker();
|
||||
};
|
||||
xhr.onload = function (e) {
|
||||
try { orz(e); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
|
||||
@@ -1563,7 +1658,7 @@ function up2k_init(subtle) {
|
||||
err = rsp;
|
||||
ofs = err.indexOf('\n/');
|
||||
if (ofs !== -1) {
|
||||
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2)).join(' ');
|
||||
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' ');
|
||||
}
|
||||
}
|
||||
if (err != "") {
|
||||
@@ -1757,42 +1852,21 @@ function up2k_init(subtle) {
|
||||
bumpthread({ "target": 1 })
|
||||
}
|
||||
|
||||
function tgl_multitask() {
|
||||
multitask = !multitask;
|
||||
bcfg_set('multitask', multitask);
|
||||
}
|
||||
|
||||
function tgl_ask_up() {
|
||||
ask_up = !ask_up;
|
||||
bcfg_set('ask_up', ask_up);
|
||||
}
|
||||
|
||||
function tgl_fsearch() {
|
||||
set_fsearch(!fsearch);
|
||||
}
|
||||
|
||||
function tgl_turbo() {
|
||||
turbo = !turbo;
|
||||
bcfg_set('u2turbo', turbo);
|
||||
draw_turbo();
|
||||
}
|
||||
|
||||
function tgl_datechk() {
|
||||
datechk = !datechk;
|
||||
bcfg_set('u2tdate', datechk);
|
||||
set_fsearch(!uc.fsearch);
|
||||
}
|
||||
|
||||
function draw_turbo() {
|
||||
var msgu = '<p class="warn">WARNING: turbo enabled, <span> client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>',
|
||||
msgs = '<p class="warn">WARNING: turbo enabled, <span> search results can be incorrect; see turbo-button tooltip</span></p>',
|
||||
msg = fsearch ? msgs : msgu,
|
||||
omsg = fsearch ? msgu : msgs,
|
||||
msg = uc.fsearch ? msgs : msgu,
|
||||
omsg = uc.fsearch ? msgu : msgs,
|
||||
html = ebi('u2foot').innerHTML,
|
||||
ohtml = html;
|
||||
|
||||
if (turbo && html.indexOf(msg) === -1)
|
||||
if (uc.turbo && html.indexOf(msg) === -1)
|
||||
html = html.replace(omsg, '') + msg;
|
||||
else if (!turbo)
|
||||
else if (!uc.turbo)
|
||||
html = html.replace(msgu, '').replace(msgs, '');
|
||||
|
||||
if (html !== ohtml)
|
||||
@@ -1818,8 +1892,8 @@ function up2k_init(subtle) {
|
||||
}
|
||||
|
||||
if (new_state !== undefined) {
|
||||
fsearch = new_state;
|
||||
bcfg_set('fsearch', fsearch);
|
||||
uc.fsearch = new_state;
|
||||
bcfg_set('fsearch', uc.fsearch);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1828,10 +1902,10 @@ function up2k_init(subtle) {
|
||||
catch (ex) { }
|
||||
|
||||
try {
|
||||
var ico = fsearch ? '🔎' : '🚀',
|
||||
desc = fsearch ? 'Search' : 'Upload';
|
||||
var ico = uc.fsearch ? '🔎' : '🚀',
|
||||
desc = uc.fsearch ? 'Search' : 'Upload';
|
||||
|
||||
clmod(ebi('op_up2k'), 'srch', fsearch);
|
||||
clmod(ebi('op_up2k'), 'srch', uc.fsearch);
|
||||
ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>';
|
||||
}
|
||||
catch (ex) { }
|
||||
@@ -1840,14 +1914,8 @@ function up2k_init(subtle) {
|
||||
onresize();
|
||||
}
|
||||
|
||||
function tgl_flag_en() {
|
||||
flag_en = !flag_en;
|
||||
bcfg_set('flag_en', flag_en);
|
||||
apply_flag_cfg();
|
||||
}
|
||||
|
||||
function apply_flag_cfg() {
|
||||
if (flag_en && !flag) {
|
||||
if (uc.flag_en && !flag) {
|
||||
try {
|
||||
flag = up2k_flagbus();
|
||||
}
|
||||
@@ -1856,7 +1924,7 @@ function up2k_init(subtle) {
|
||||
tgl_flag_en();
|
||||
}
|
||||
}
|
||||
else if (!flag_en && flag) {
|
||||
else if (!uc.flag_en && flag) {
|
||||
if (flag.ours)
|
||||
flag.give();
|
||||
|
||||
@@ -1880,17 +1948,10 @@ function up2k_init(subtle) {
|
||||
};
|
||||
|
||||
ebi('nthread').onkeydown = bumpthread2;
|
||||
ebi('nthread').addEventListener('input', bumpthread, false);
|
||||
ebi('multitask').addEventListener('click', tgl_multitask, false);
|
||||
ebi('ask_up').addEventListener('click', tgl_ask_up, false);
|
||||
ebi('flag_en').addEventListener('click', tgl_flag_en, false);
|
||||
ebi('u2turbo').addEventListener('click', tgl_turbo, false);
|
||||
ebi('u2tdate').addEventListener('click', tgl_datechk, false);
|
||||
var o = ebi('fsearch');
|
||||
if (o)
|
||||
o.addEventListener('click', tgl_fsearch, false);
|
||||
ebi('nthread').oninput = bumpthread;
|
||||
|
||||
ebi('u2etas').onclick = function () {
|
||||
ebi('u2etas').onclick = function (e) {
|
||||
ev(e);
|
||||
clmod(ebi('u2etas'), 'o', 't');
|
||||
};
|
||||
|
||||
@@ -1899,7 +1960,7 @@ function up2k_init(subtle) {
|
||||
if (parallel_uploads < 1)
|
||||
bumpthread(1);
|
||||
|
||||
return { "init_deps": init_deps, "set_fsearch": set_fsearch }
|
||||
return { "init_deps": init_deps, "set_fsearch": set_fsearch, "ui": pvis }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ if (!window['console'])
|
||||
|
||||
var is_touch = 'ontouchstart' in window,
|
||||
IPHONE = /iPhone|iPad|iPod/i.test(navigator.userAgent),
|
||||
ANDROID = /android/i.test(navigator.userAgent);
|
||||
ANDROID = /android/i.test(navigator.userAgent),
|
||||
WINDOWS = navigator.platform ? navigator.platform == 'Win32' : /Windows/.test(navigator.userAgent);
|
||||
|
||||
|
||||
var ebi = document.getElementById.bind(document),
|
||||
@@ -28,6 +29,44 @@ function esc(txt) {
|
||||
}[c];
|
||||
});
|
||||
}
|
||||
window.onunhandledrejection = function (e) {
|
||||
var err = e.reason;
|
||||
try {
|
||||
err += '\n' + e.reason.stack;
|
||||
}
|
||||
catch (e) { }
|
||||
console.log("REJ: " + err);
|
||||
try {
|
||||
toast.warn(30, err);
|
||||
}
|
||||
catch (e) { }
|
||||
};
|
||||
try {
|
||||
console.hist = [];
|
||||
var hook = function (t) {
|
||||
var orig = console[t].bind(console),
|
||||
cfun = function () {
|
||||
console.hist.push(Date.now() + ' ' + t + ': ' + Array.from(arguments).join(', '));
|
||||
if (console.hist.length > 100)
|
||||
console.hist = console.hist.slice(50);
|
||||
|
||||
orig.apply(console, arguments);
|
||||
};
|
||||
|
||||
console['std' + t] = orig;
|
||||
console[t] = cfun;
|
||||
};
|
||||
hook('log');
|
||||
console.log('log-capture ok');
|
||||
hook('debug');
|
||||
hook('warn');
|
||||
hook('error');
|
||||
}
|
||||
catch (ex) {
|
||||
if (console.stdlog)
|
||||
console.log = console.stdlog;
|
||||
console.log(ex);
|
||||
}
|
||||
var crashed = false, ignexd = {};
|
||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
if ((msg + '').indexOf('ResizeObserver') !== -1)
|
||||
@@ -39,23 +78,59 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
|
||||
crashed = true;
|
||||
window.onerror = undefined;
|
||||
var con = is_touch ? '' : '<br /> (and if you can, press F12 and include the "Console" tab in the screenshot too)',
|
||||
html = ['<h1>you hit a bug!</h1><p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a></p><p>please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code>' + con + '</p><p>',
|
||||
esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>'];
|
||||
var html = [
|
||||
'<h1>you hit a bug!</h1>',
|
||||
'<p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a></p>',
|
||||
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">github issue</a> or <code>ed#2644</code></p>',
|
||||
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>',
|
||||
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
||||
];
|
||||
|
||||
try {
|
||||
var ua = '',
|
||||
ad = navigator.userAgentData,
|
||||
adb = ad.brands;
|
||||
|
||||
for (var a = 0; a < adb.length; a++)
|
||||
if (!/Not.*A.*Brand/.exec(adb[a].brand))
|
||||
ua += adb[a].brand + '/' + adb[a].version + ', ';
|
||||
ua += ad.platform;
|
||||
|
||||
html.push('<br /><b>UAD:</b> ' + esc(ua.slice(0, 100)));
|
||||
}
|
||||
catch (e) { }
|
||||
html.push('</p>');
|
||||
|
||||
try {
|
||||
if (error) {
|
||||
var find = ['desc', 'stack', 'trace'];
|
||||
for (var a = 0; a < find.length; a++)
|
||||
if (String(error[find[a]]) !== 'undefined')
|
||||
html.push('<h3>' + find[a] + '</h3>' +
|
||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
||||
html.push('<p class="b"><b>' + find[a] + ':</b><br />' +
|
||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n') + '</p>');
|
||||
}
|
||||
ignexd[ekey] = true;
|
||||
html.push('<h3>localStore</h3>' + esc(JSON.stringify(localStorage)));
|
||||
|
||||
var ls = jcp(localStorage);
|
||||
if (ls.fman_clip)
|
||||
ls.fman_clip = ls.fman_clip.length + ' items';
|
||||
|
||||
var lsk = Object.keys(ls);
|
||||
lsk.sort();
|
||||
html.push('<p class="b">');
|
||||
for (var a = 0; a < lsk.length; a++)
|
||||
html.push(' <b>' + esc(lsk[a]) + '</b> <code>' + esc(ls[lsk[a]]) + '</code> ');
|
||||
html.push('</p>');
|
||||
}
|
||||
catch (e) { }
|
||||
|
||||
if (console.hist.length) {
|
||||
html.push('<p class="b"><b>console:</b><ul><li>' + Date.now() + ' @</li>');
|
||||
for (var a = console.hist.length - 1, aa = Math.max(0, console.hist.length - 20); a >= aa; a--)
|
||||
html.push('<li>' + esc(console.hist[a]) + '</li>');
|
||||
html.push('</ul>')
|
||||
}
|
||||
|
||||
try {
|
||||
var exbox = ebi('exbox');
|
||||
if (!exbox) {
|
||||
@@ -64,10 +139,19 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
document.body.appendChild(exbox);
|
||||
|
||||
var s = mknod('style');
|
||||
s.innerHTML = '#exbox{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} #exbox h1{margin:.5em 1em 0 0;padding:0} #exbox h3{border-top:1px solid #999;margin:1em 0 0 0} #exbox a{text-decoration:underline;color:#fc0} #exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} #exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word}';
|
||||
s.innerHTML = (
|
||||
'#exbox{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
|
||||
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
|
||||
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
|
||||
'#exbox a{text-decoration:underline;color:#fc0} ' +
|
||||
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
|
||||
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
|
||||
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
|
||||
'#exbox b{color:#fff}'
|
||||
);
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
exbox.innerHTML = html.join('\n').replace(/https?:\/\/[^ \/]+\//g, '/');
|
||||
exbox.innerHTML = html.join('\n').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js').replace(/<ghi>/, 'https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md');
|
||||
exbox.style.display = 'block';
|
||||
}
|
||||
catch (e) {
|
||||
@@ -85,6 +169,9 @@ function ignex(all) {
|
||||
}
|
||||
|
||||
|
||||
function noop() { }
|
||||
|
||||
|
||||
function ctrl(e) {
|
||||
return e && (e.ctrlKey || e.metaKey);
|
||||
}
|
||||
@@ -110,36 +197,40 @@ function ev(e) {
|
||||
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
|
||||
if (!String.prototype.endsWith) {
|
||||
if (!String.prototype.endsWith)
|
||||
String.prototype.endsWith = function (search, this_len) {
|
||||
if (this_len === undefined || this_len > this.length) {
|
||||
this_len = this.length;
|
||||
}
|
||||
return this.substring(this_len - search.length, this_len) === search;
|
||||
};
|
||||
}
|
||||
if (!String.startsWith) {
|
||||
|
||||
if (!String.startsWith)
|
||||
String.prototype.startsWith = function (s, i) {
|
||||
i = i > 0 ? i | 0 : 0;
|
||||
return this.substring(i, i + s.length) === s;
|
||||
};
|
||||
}
|
||||
if (!Element.prototype.matches) {
|
||||
|
||||
if (!String.trimEnd)
|
||||
String.prototype.trimEnd = String.prototype.trimRight = function () {
|
||||
return this.replace(/[ \t\r\n]+$/m, '');
|
||||
};
|
||||
|
||||
if (!Element.prototype.matches)
|
||||
Element.prototype.matches =
|
||||
Element.prototype.oMatchesSelector ||
|
||||
Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.mozMatchesSelector ||
|
||||
Element.prototype.webkitMatchesSelector;
|
||||
}
|
||||
if (!Element.prototype.closest) {
|
||||
|
||||
if (!Element.prototype.closest)
|
||||
Element.prototype.closest = function (s) {
|
||||
var el = this;
|
||||
do {
|
||||
if (el.matches(s)) return el;
|
||||
el = el.parentElement || el.parentNode;
|
||||
} while (el !== null && el.nodeType === 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// https://stackoverflow.com/a/950146
|
||||
@@ -286,8 +377,16 @@ function makeSortable(table, cb) {
|
||||
|
||||
|
||||
function linksplit(rp) {
|
||||
var ret = [];
|
||||
var apath = '/';
|
||||
var ret = [],
|
||||
apath = '/',
|
||||
q = null;
|
||||
|
||||
if (rp && rp.indexOf('?') + 1) {
|
||||
q = rp.split('?', 2);
|
||||
rp = q[0];
|
||||
q = '?' + q[1];
|
||||
}
|
||||
|
||||
if (rp && rp.charAt(0) == '/')
|
||||
rp = rp.slice(1);
|
||||
|
||||
@@ -309,6 +408,9 @@ function linksplit(rp) {
|
||||
elink = elink.slice(0, -3) + '/';
|
||||
}
|
||||
|
||||
if (!rp && q)
|
||||
elink += q;
|
||||
|
||||
ret.push('<a href="' + apath + elink + '">' + vlink + '</a>');
|
||||
apath += elink;
|
||||
}
|
||||
@@ -392,6 +494,11 @@ function get_vpath() {
|
||||
}
|
||||
|
||||
|
||||
function noq_href(el) {
|
||||
return el.getAttribute('href').split('?')[0];
|
||||
}
|
||||
|
||||
|
||||
function get_pwd() {
|
||||
var pwd = ('; ' + document.cookie).split('; cppwd=');
|
||||
if (pwd.length < 2)
|
||||
@@ -545,6 +652,24 @@ function bcfg_upd_ui(name, val) {
|
||||
}
|
||||
}
|
||||
|
||||
function bcfg_bind(obj, oname, cname, defval, cb, un_ev) {
|
||||
var v = bcfg_get(cname, defval),
|
||||
el = ebi(cname);
|
||||
|
||||
obj[oname] = v;
|
||||
if (el)
|
||||
el.onclick = function (e) {
|
||||
if (un_ev !== false)
|
||||
ev(e);
|
||||
|
||||
obj[oname] = bcfg_set(cname, !obj[oname]);
|
||||
if (cb)
|
||||
cb(obj[oname]);
|
||||
};
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
function hist_push(url) {
|
||||
console.log("h-push " + url);
|
||||
@@ -575,6 +700,9 @@ var timer = (function () {
|
||||
};
|
||||
|
||||
function doevents() {
|
||||
if (crashed)
|
||||
return;
|
||||
|
||||
if (Date.now() - r.last < 69)
|
||||
return;
|
||||
|
||||
@@ -601,6 +729,14 @@ var tt = (function () {
|
||||
r.tt.setAttribute('id', 'tt');
|
||||
document.body.appendChild(r.tt);
|
||||
|
||||
var prev = null;
|
||||
r.cshow = function () {
|
||||
if (this !== prev)
|
||||
r.show.bind(this)();
|
||||
|
||||
prev = this;
|
||||
};
|
||||
|
||||
r.show = function () {
|
||||
if (r.skip) {
|
||||
r.skip = false;
|
||||
@@ -620,6 +756,7 @@ var tt = (function () {
|
||||
r.el = this;
|
||||
var pos = this.getBoundingClientRect(),
|
||||
dir = this.getAttribute('ttd') || '',
|
||||
margin = parseFloat(this.getAttribute('ttm') || 0),
|
||||
top = pos.top < window.innerHeight / 2,
|
||||
big = this.className.indexOf(' ttb') !== -1;
|
||||
|
||||
@@ -631,29 +768,29 @@ var tt = (function () {
|
||||
r.tt.style.top = '0';
|
||||
|
||||
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
||||
var tw = r.tt.offsetWidth,
|
||||
x = pos.left + (pos.right - pos.left) / 2 - tw / 2;
|
||||
|
||||
if (x < 0)
|
||||
x = 8;
|
||||
|
||||
if (x + tw >= window.innerWidth - 8) {
|
||||
x = window.innerWidth - tw - 8;
|
||||
}
|
||||
|
||||
r.tt.style.left = x + 'px';
|
||||
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
|
||||
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
|
||||
|
||||
r.el.addEventListener('mouseleave', r.hide);
|
||||
window.addEventListener('scroll', r.hide);
|
||||
clmod(r.tt, 'show', 1);
|
||||
|
||||
var tw = r.tt.offsetWidth,
|
||||
x = pos.left + (pos.right - pos.left) / 2 - tw / 2;
|
||||
|
||||
if (x + tw >= window.innerWidth - 24)
|
||||
x = window.innerWidth - tw - 24;
|
||||
|
||||
if (x < 0)
|
||||
x = 12;
|
||||
|
||||
r.tt.style.left = x + 'px';
|
||||
r.tt.style.top = top ? (margin + pos.bottom) + 'px' : 'auto';
|
||||
r.tt.style.bottom = top ? 'auto' : (margin + window.innerHeight - pos.top) + 'px';
|
||||
};
|
||||
|
||||
r.hide = function (e) {
|
||||
ev(e);
|
||||
window.removeEventListener('scroll', r.hide);
|
||||
clmod(r.tt, 'show');
|
||||
clmod(r.tt, 'b');
|
||||
if (r.el)
|
||||
r.el.removeEventListener('mouseleave', r.hide);
|
||||
};
|
||||
@@ -683,12 +820,13 @@ var tt = (function () {
|
||||
r.tt.onclick = r.hide;
|
||||
|
||||
r.att = function (ctr) {
|
||||
var _show = r.en ? r.show : null,
|
||||
var _cshow = r.en ? r.cshow : null,
|
||||
_show = r.en ? r.show : null,
|
||||
_hide = r.en ? r.hide : null,
|
||||
o = ctr.querySelectorAll('*[tt]');
|
||||
|
||||
for (var a = o.length - 1; a >= 0; a--) {
|
||||
o[a].onfocus = _show;
|
||||
o[a].onfocus = _cshow;
|
||||
o[a].onblur = _hide;
|
||||
o[a].onmouseenter = _show;
|
||||
o[a].onmouseleave = _hide;
|
||||
@@ -727,6 +865,7 @@ function lf2br(txt) {
|
||||
var toast = (function () {
|
||||
var r = {},
|
||||
te = null,
|
||||
scrolling = false,
|
||||
obj = mknod('div');
|
||||
|
||||
obj.setAttribute('id', 'toast');
|
||||
@@ -734,38 +873,61 @@ var toast = (function () {
|
||||
r.visible = false;
|
||||
r.txt = null;
|
||||
|
||||
function scrollchk() {
|
||||
if (scrolling)
|
||||
return;
|
||||
|
||||
var tb = ebi('toastb'),
|
||||
vis = tb.offsetHeight,
|
||||
all = tb.scrollHeight;
|
||||
|
||||
if (8 + vis >= all)
|
||||
return;
|
||||
|
||||
clmod(obj, 'scroll', 1);
|
||||
scrolling = true;
|
||||
}
|
||||
|
||||
function unscroll() {
|
||||
timer.rm(scrollchk);
|
||||
clmod(obj, 'scroll');
|
||||
scrolling = false;
|
||||
}
|
||||
|
||||
r.hide = function (e) {
|
||||
ev(e);
|
||||
unscroll();
|
||||
clearTimeout(te);
|
||||
clmod(obj, 'vis');
|
||||
r.visible = false;
|
||||
};
|
||||
|
||||
r.show = function (cl, ms, txt) {
|
||||
r.show = function (cl, sec, txt) {
|
||||
clearTimeout(te);
|
||||
if (ms)
|
||||
te = setTimeout(r.hide, ms * 1000);
|
||||
if (sec)
|
||||
te = setTimeout(r.hide, sec * 1000);
|
||||
|
||||
obj.innerHTML = '<a href="#" id="toastc">x</a>' + lf2br(txt);
|
||||
obj.innerHTML = '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';
|
||||
obj.className = cl;
|
||||
ms += obj.offsetWidth;
|
||||
sec += obj.offsetWidth;
|
||||
obj.className += ' vis';
|
||||
ebi('toastc').onclick = r.hide;
|
||||
timer.add(scrollchk);
|
||||
r.visible = true;
|
||||
r.txt = txt;
|
||||
};
|
||||
|
||||
r.ok = function (ms, txt) {
|
||||
r.show('ok', ms, txt);
|
||||
r.ok = function (sec, txt) {
|
||||
r.show('ok', sec, txt);
|
||||
};
|
||||
r.inf = function (ms, txt) {
|
||||
r.show('inf', ms, txt);
|
||||
r.inf = function (sec, txt) {
|
||||
r.show('inf', sec, txt);
|
||||
};
|
||||
r.warn = function (ms, txt) {
|
||||
r.show('warn', ms, txt);
|
||||
r.warn = function (sec, txt) {
|
||||
r.show('warn', sec, txt);
|
||||
};
|
||||
r.err = function (ms, txt) {
|
||||
r.show('err', ms, txt);
|
||||
r.err = function (sec, txt) {
|
||||
r.show('err', sec, txt);
|
||||
};
|
||||
|
||||
return r;
|
||||
@@ -776,8 +938,12 @@ var modal = (function () {
|
||||
var r = {},
|
||||
q = [],
|
||||
o = null,
|
||||
cb_up = null,
|
||||
cb_ok = null,
|
||||
cb_ng = null;
|
||||
cb_ng = null,
|
||||
prim = '<a href="#" id="modal-ok">OK</a>',
|
||||
sec = '<a href="#" id="modal-ng">Cancel</a>',
|
||||
ok_cancel = WINDOWS ? prim + sec : sec + prim;
|
||||
|
||||
r.busy = false;
|
||||
|
||||
@@ -805,6 +971,8 @@ var modal = (function () {
|
||||
|
||||
document.addEventListener('focus', onfocus);
|
||||
timer.add(onfocus);
|
||||
if (cb_up)
|
||||
setTimeout(cb_up, 1);
|
||||
};
|
||||
|
||||
r.hide = function () {
|
||||
@@ -859,41 +1027,44 @@ var modal = (function () {
|
||||
q.shift()();
|
||||
}
|
||||
|
||||
r.alert = function (html, cb) {
|
||||
r.alert = function (html, cb, fun) {
|
||||
q.push(function () {
|
||||
_alert(lf2br(html), cb);
|
||||
_alert(lf2br(html), cb, fun);
|
||||
});
|
||||
next();
|
||||
};
|
||||
function _alert(html, cb) {
|
||||
function _alert(html, cb, fun) {
|
||||
cb_ok = cb_ng = cb;
|
||||
cb_up = fun;
|
||||
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a></div>';
|
||||
r.show(html);
|
||||
}
|
||||
|
||||
r.confirm = function (html, cok, cng) {
|
||||
r.confirm = function (html, cok, cng, fun) {
|
||||
q.push(function () {
|
||||
_confirm(lf2br(html), cok, cng);
|
||||
_confirm(lf2br(html), cok, cng, fun);
|
||||
});
|
||||
next();
|
||||
}
|
||||
function _confirm(html, cok, cng) {
|
||||
function _confirm(html, cok, cng, fun) {
|
||||
cb_ok = cok;
|
||||
cb_ng = cng === undefined ? cok : null;
|
||||
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a><a href="#" id="modal-ng">Cancel</a></div>';
|
||||
cb_up = fun;
|
||||
html += '<div id="modalb">' + ok_cancel + '</div>';
|
||||
r.show(html);
|
||||
}
|
||||
|
||||
r.prompt = function (html, v, cok, cng) {
|
||||
r.prompt = function (html, v, cok, cng, fun) {
|
||||
q.push(function () {
|
||||
_prompt(lf2br(html), v, cok, cng);
|
||||
_prompt(lf2br(html), v, cok, cng, fun);
|
||||
});
|
||||
next();
|
||||
}
|
||||
function _prompt(html, v, cok, cng) {
|
||||
function _prompt(html, v, cok, cng, fun) {
|
||||
cb_ok = cok;
|
||||
cb_ng = cng === undefined ? cok : null;
|
||||
html += '<input id="modali" type="text" /><div id="modalb"><a href="#" id="modal-ok">OK</a><a href="#" id="modal-ng">Cancel</a></div>';
|
||||
cb_up = fun;
|
||||
html += '<input id="modali" type="text" /><div id="modalb">' + ok_cancel + '</div>';
|
||||
r.show(html);
|
||||
|
||||
ebi('modali').value = v || '';
|
||||
@@ -914,9 +1085,69 @@ function winpopup(txt) {
|
||||
}
|
||||
|
||||
|
||||
var last_repl = null;
|
||||
function repl_load() {
|
||||
var ipre = ebi('repl_pre'),
|
||||
tb = ebi('modali');
|
||||
|
||||
function getpres() {
|
||||
var o, ret = jread("repl_pre", []);
|
||||
if (!ret.length)
|
||||
ret = [
|
||||
'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)',
|
||||
'console.hist.slice(-10).join("\\n")'
|
||||
];
|
||||
|
||||
ipre.innerHTML = '<option value=""></option>';
|
||||
for (var a = 0; a < ret.length; a++) {
|
||||
o = mknod('option');
|
||||
o.setAttribute('value', ret[a]);
|
||||
o.textContent = ret[a];
|
||||
ipre.appendChild(o);
|
||||
}
|
||||
last_repl = ipre.value = (last_repl || (ret.length ? ret.slice(-1)[0] : ''));
|
||||
return ret;
|
||||
}
|
||||
ebi('repl_pdel').onclick = function (e) {
|
||||
var val = ipre.value,
|
||||
pres = getpres();
|
||||
|
||||
apop(pres, val);
|
||||
jwrite('repl_pre', pres);
|
||||
getpres();
|
||||
};
|
||||
ebi('repl_pnew').onclick = function (e) {
|
||||
var val = tb.value,
|
||||
pres = getpres();
|
||||
|
||||
apop(pres, ipre.value);
|
||||
pres.push(val);
|
||||
jwrite('repl_pre', pres);
|
||||
getpres();
|
||||
ipre.value = val;
|
||||
};
|
||||
ipre.oninput = ipre.onchange = function () {
|
||||
tb.value = last_repl = ipre.value;
|
||||
};
|
||||
tb.oninput = function () {
|
||||
last_repl = this.value;
|
||||
};
|
||||
getpres();
|
||||
tb.value = last_repl;
|
||||
setTimeout(function () {
|
||||
tb.setSelectionRange(0, tb.value.length, "forward");
|
||||
}, 10);
|
||||
}
|
||||
function repl(e) {
|
||||
ev(e);
|
||||
modal.prompt('js repl (prefix with <code>,</code> to allow raise)', 'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)', function (cmd) {
|
||||
var html = [
|
||||
'<p>js repl (prefix with <code>,</code> to allow raise)</p>',
|
||||
'<p><select id="repl_pre"></select>',
|
||||
' <button id="repl_pdel">❌ del</button>',
|
||||
' <button id="repl_pnew">💾 SAVE</button></p>'
|
||||
];
|
||||
|
||||
modal.prompt(html.join(''), '', function (cmd) {
|
||||
if (!cmd)
|
||||
return toast.inf(3, 'eval aborted');
|
||||
|
||||
@@ -929,7 +1160,7 @@ function repl(e) {
|
||||
catch (ex) {
|
||||
modal.alert('<h6>exception</h6>' + esc(ex + ''));
|
||||
}
|
||||
});
|
||||
}, undefined, repl_load);
|
||||
}
|
||||
if (ebi('repl'))
|
||||
ebi('repl').onclick = repl;
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
# example `.epilogue.html`
|
||||
**NOTE:** there's more stuff (sharex config, service scripts, nginx configs, ...) in [`/contrib/`](/contrib/)
|
||||
|
||||
|
||||
|
||||
# example resource files
|
||||
|
||||
can be provided to copyparty to tweak things
|
||||
|
||||
|
||||
|
||||
## example `.epilogue.html`
|
||||
save one of these as `.epilogue.html` inside a folder to customize it:
|
||||
|
||||
* [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||
|
||||
|
||||
|
||||
# example browser-css
|
||||
## example browser-css
|
||||
point `--css-browser` to one of these by URL:
|
||||
|
||||
* [`browser.css`](browser.css) changes the background
|
||||
@@ -19,4 +29,23 @@ point `--css-browser` to one of these by URL:
|
||||
* notes on using rclone as a fuse client/server
|
||||
|
||||
## [`example.conf`](example.conf)
|
||||
* example config file for `-c` which never really happened
|
||||
* example config file for `-c` (supports accounts, volumes, and volume-flags)
|
||||
|
||||
|
||||
|
||||
# junk
|
||||
|
||||
alphabetical list of the remaining files
|
||||
|
||||
| what | why |
|
||||
| -- | -- |
|
||||
| [biquad.html](biquad.html) | bruteforce calibrator for the audio equalizer since im not that good at maths |
|
||||
| [design.txt](design.txt) | initial brainstorming of the copyparty design, unmaintained, incorrect, sentimental value only |
|
||||
| [hls.html](hls.html) | experimenting with hls playback using `hls.js`, works p well, almost became a thing |
|
||||
| [music-analysis.sh](music-analysis.sh) | testing various bpm/key detection libraries before settling on the ones used in [`/bin/mtag/`](/bin/mtag/) |
|
||||
| [notes.sh](notes.sh) | notepad, just scraps really |
|
||||
| [nuitka.txt](nuitka.txt) | how to build a copyparty exe using nuitka (not maintained) |
|
||||
| [pretend-youre-qnap.patch](pretend-youre-qnap.patch) | simulate a NAS which keeps returning old cached data even though you just modified the file yourself |
|
||||
| [tcp-debug.sh](tcp-debug.sh) | looks like this was to debug stuck tcp connections? |
|
||||
| [unirange.py](unirange.py) | uhh |
|
||||
| [up2k.txt](up2k.txt) | initial ideas for how up2k should work, another unmaintained sentimental-value-only thing |
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
setTimeout(location.reload.bind(location), 700);
|
||||
document.documentElement.scrollLeft = 0;
|
||||
|
||||
var cali = (function() {
|
||||
var ac = new AudioContext(),
|
||||
fi = ac.createBiquadFilter(),
|
||||
freqs = new Float32Array(1),
|
||||
mag = new Float32Array(1),
|
||||
phase = new Float32Array(1);
|
||||
|
||||
freqs[0] = 14000;
|
||||
fi.type = 'peaking';
|
||||
fi.frequency.value = 18000;
|
||||
fi.Q.value = 0.8;
|
||||
fi.gain.value = 1;
|
||||
fi.getFrequencyResponse(freqs, mag, phase);
|
||||
|
||||
return mag[0]; // 1.0407 good, 1.0563 bad
|
||||
})(),
|
||||
mp = cali < 1.05;
|
||||
|
||||
var can = document.createElement('canvas'),
|
||||
cc = can.getContext('2d'),
|
||||
w = 2048,
|
||||
@@ -28,12 +46,12 @@ var cfg = [ // hz, q, g
|
||||
[1000, 0.9, 1.1],
|
||||
[2000, 0.9, 1.105],
|
||||
[4000, 0.88, 1.05],
|
||||
[8000 * 1.006, 0.73, 1.24],
|
||||
[8000 * 1.006, 0.73, mp ? 1.24 : 1.2],
|
||||
//[16000 * 1.00, 0.5, 1.75], // peak.v1
|
||||
//[16000 * 1.19, 0, 1.8] // shelf.v1
|
||||
[16000 * 0.89, 0.7, 1.26], // peak
|
||||
[16000 * 1.13, 0.82, 1.09], // peak
|
||||
[16000 * 1.205, 0, 1.9] // shelf
|
||||
[16000 * 0.89, 0.7, mp ? 1.26 : 1.2], // peak
|
||||
[16000 * 1.13, 0.82, mp ? 1.09 : 0.75], // peak
|
||||
[16000 * 1.205, 0, mp ? 1.9 : 1.85] // shelf
|
||||
];
|
||||
|
||||
var freqs = new Float32Array(22000),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* video */
|
||||
/* video, alternative 1:
|
||||
top-left icon, just like the other formats
|
||||
=======================================================================
|
||||
|
||||
#ggrid>a:is(
|
||||
[href$=".mkv"i],
|
||||
[href$=".mp4"i],
|
||||
@@ -6,6 +9,40 @@
|
||||
):before {
|
||||
content: '📺';
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* video, alternative 2:
|
||||
play-icon in the middle of the thumbnail
|
||||
=======================================================================
|
||||
*/
|
||||
#ggrid>a:is(
|
||||
[href$=".mkv"i],
|
||||
[href$=".mp4"i],
|
||||
[href$=".webm"i],
|
||||
) {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#ggrid>a:is(
|
||||
[href$=".mkv"i],
|
||||
[href$=".mp4"i],
|
||||
[href$=".webm"i],
|
||||
):before {
|
||||
content: '▶';
|
||||
opacity: .8;
|
||||
margin: 0;
|
||||
padding: 1em .5em 1em .7em;
|
||||
border-radius: 9em;
|
||||
line-height: 0;
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
left: calc(50% - 1em);
|
||||
top: calc(50% - 1.4em);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* audio */
|
||||
@@ -21,6 +58,7 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* image */
|
||||
#ggrid>a:is(
|
||||
[href$=".jpg"i],
|
||||
|
||||
@@ -17,6 +17,7 @@ html.light {
|
||||
html.light #files th {
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
html.light .logue,
|
||||
html.light #ops,
|
||||
html.light #treeul,
|
||||
html.light #files td {
|
||||
|
||||
@@ -47,5 +47,5 @@ c e2d
|
||||
c nodupe
|
||||
|
||||
# this entire config file can be replaced with these arguments:
|
||||
# -u ed:123 -u k:k -v .::r:a,ed -v priv:priv:r,k:rw,ed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w:c,e2d:c,nodupe
|
||||
# -u ed:123 -u k:k -v .::r:a,ed -v priv:priv:r,k:rw,ed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w:c,e2d,nodupe
|
||||
# but note that the config file always wins in case of conflicts
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
||||
|
||||
#srch_dz, #srch_zd, /* the filesearch dropzone */
|
||||
|
||||
#u2cards, #u2etaw /* and the upload progress tabs */
|
||||
|
||||
{display: none !important} /* do it! */
|
||||
@@ -27,6 +29,9 @@
|
||||
/* adjust the button area a bit */
|
||||
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
|
||||
|
||||
/* a */
|
||||
#op_up2k {min-height: 0}
|
||||
|
||||
</style>
|
||||
|
||||
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
method = self.s.recv(4)
|
||||
self.s.unrecv(method)
|
||||
print("xxx unrecv'd [{}]".format(method))
|
||||
|
||||
# jython used to do this, they stopped since it's broken
|
||||
# but reimplementing sendall is out of scope for now
|
||||
if not getattr(self.s.s, "sendall", None):
|
||||
self.s.s.sendall = self.s.s.send
|
||||
|
||||
# TODO this is also pretty bad
|
||||
have = dir(self.s)
|
||||
for k in self.s.s.__dict__:
|
||||
if k not in have and not k.startswith("__"):
|
||||
if k == "recv":
|
||||
raise Exception("wait what")
|
||||
|
||||
self.s.__dict__[k] = self.s.s.__dict__[k]
|
||||
|
||||
have = dir(self.s)
|
||||
for k in dir(self.s.s):
|
||||
if k not in have and not k.startswith("__"):
|
||||
if k == "recv":
|
||||
raise Exception("wait what")
|
||||
|
||||
setattr(self.s, k, getattr(self.s.s, k))
|
||||
@@ -41,9 +41,9 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
|
||||
##
|
||||
## bad filenames
|
||||
|
||||
dirs=("$HOME/vfs/ほげ" "$HOME/vfs/ほげ/ぴよ" "$HOME/vfs/$(printf \\xed\\x91)" "$HOME/vfs/$(printf \\xed\\x91/\\xed\\x92)")
|
||||
dirs=("./ほげ" "./ほげ/ぴよ" "./$(printf \\xed\\x91)" "./$(printf \\xed\\x91/\\xed\\x92)" './qw,er;ty%20as df?gh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd fgh')
|
||||
mkdir -p "${dirs[@]}"
|
||||
for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd fgh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd fgh'; do echo "$dir" > "$dir/$fn.html"; done; done
|
||||
for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qw,er;ty%20as df?gh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd fgh'; do echo "$dir" > "$dir/$fn.html"; done; done
|
||||
# qw er+ty%20ui%%20op<as>df&gh&jk#zx'cv"bn`m=qw*er^ty?ui@op,as.df-gh_jk
|
||||
|
||||
##
|
||||
@@ -79,10 +79,8 @@ command -v gdate && date() { gdate "$@"; }; while true; do t=$(date +%s.%N); (ti
|
||||
# get all up2k search result URLs
|
||||
var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute("href")))}); console.log(t.join("\n"));
|
||||
|
||||
# rename all selected songs to <leading-track-number> + <Title> + <extension>
|
||||
var sel=msel.getsel(), ci=find_file_col('Title')[0], re=[]; for (var a=0; a<sel.length; a++) { var url=sel[a].vp, tag=ebi(sel[a].id).closest('tr').querySelectorAll('td')[ci].textContent, name=uricom_dec(vsplit(url)[1])[0], m=/^([0-9]+[\. -]+)?.*(\.[^\.]+$)/.exec(name), name2=(m[1]||'')+tag+m[2], url2=vsplit(url)[0]+uricom_enc(name2,false); if (url!=url2) re.push([url, url2]); }
|
||||
console.log(JSON.stringify(re, null, ' '));
|
||||
function f() { if (!re.length) return treectl.goto(get_evpath()); var [u1,u2] = re.shift(); fetch(u1+'?move='+u2).then((rsp) => {if (rsp.ok) f(); }); }; f();
|
||||
# debug md-editor line tracking
|
||||
var s=mknod('style');s.innerHTML='*[data-ln]:before {content:attr(data-ln)!important;color:#f0c;background:#000;position:absolute;left:-1.5em;font-size:1rem}';document.head.appendChild(s);
|
||||
|
||||
##
|
||||
## bash oneliners
|
||||
@@ -130,6 +128,9 @@ sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /de
|
||||
for ((f=420;f<1200;f++)); do sz=$(ffmpeg -y -f lavfi -i sine=frequency=$f:duration=2 -vf volume=0.1 -ac 1 -ar 44100 -f s16le /dev/shm/a.wav 2>/dev/null; base64 -w0 </dev/shm/a.wav | gzip -c | wc -c); printf '%d %d\n' $f $sz; done | tee /dev/stderr | sort -nrk2,2
|
||||
ffmpeg -y -f lavfi -i sine=frequency=1050:duration=2 -vf volume=0.1 -ac 1 -ar 44100 /dev/shm/a.wav
|
||||
|
||||
# play icon calibration pics
|
||||
for w in 150 170 190 210 230 250; do for h in 130 150 170 190 210; do /c/Program\ Files/ImageMagick-7.0.11-Q16-HDRI/magick.exe convert -size ${w}x${h} xc:brown -fill orange -draw "circle $((w/2)),$((h/2)) $((w/2)),$((h/3))" $w-$h.png; done; done
|
||||
|
||||
|
||||
##
|
||||
## vscode
|
||||
@@ -161,7 +162,7 @@ brew install python@2
|
||||
pip install virtualenv
|
||||
|
||||
# readme toc
|
||||
cat README.md | awk 'function pr() { if (!h) {return}; if (/^ *[*!#]/||!s) {printf "%s\n",h;h=0;return}; if (/.../) {printf "%s - %s\n",h,$0;h=0}; }; /^#/{s=1;pr()} /^#* *(file indexing|install on android|dev env setup|just the sfx|complete release|optional gpl stuff)|`$/{s=0} /^#/{lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab); h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);next} !h{next} {sub(/:$/,"")} {pr()}' > toc; grep -E '^## readme toc' -B1000 -A2 <README.md >p1; grep -E '^## quickstart' -B2 -A999999 <README.md >p2; (cat p1; grep quickstart -A1000 <toc; cat p2) >README.md
|
||||
cat README.md | awk 'function pr() { if (!h) {return}; if (/^ *[*!#]/||!s) {printf "%s\n",h;h=0;return}; if (/.../) {printf "%s - %s\n",h,$0;h=0}; }; /^#/{s=1;pr()} /^#* *(file indexing|install on android|dev env setup|just the sfx|complete release|optional gpl stuff)|`$/{s=0} /^#/{lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab); h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);next} !h{next} {sub(/ .*/,"");sub(/[:,]$/,"")} {pr()}' > toc; grep -E '^## readme toc' -B1000 -A2 <README.md >p1; grep -E '^## quickstart' -B2 -A999999 <README.md >p2; (cat p1; grep quickstart -A1000 <toc; cat p2) >README.md
|
||||
|
||||
# fix firefox phantom breakpoints,
|
||||
# suggestions from bugtracker, doesnt work (debugger is not attachable)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM alpine:3.13
|
||||
FROM alpine:3.14
|
||||
WORKDIR /z
|
||||
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
||||
ver_hashwasm=4.7.0 \
|
||||
ver_marked=1.1.0 \
|
||||
ver_hashwasm=4.9.0 \
|
||||
ver_marked=3.0.4 \
|
||||
ver_ogvjs=1.8.4 \
|
||||
ver_mde=2.14.0 \
|
||||
ver_codemirror=5.59.3 \
|
||||
ver_mde=2.15.0 \
|
||||
ver_codemirror=5.62.3 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
ver_zopfli=1.0.3
|
||||
|
||||
@@ -113,9 +113,10 @@ RUN cd CodeMirror-$ver_codemirror \
|
||||
COPY easymde.patch /z/
|
||||
RUN cd easy-markdown-editor-$ver_mde \
|
||||
&& patch -p1 < /z/easymde.patch \
|
||||
&& sed -ri 's`https://registry.npmjs.org/marked/-/marked-0.8.2.tgz`file:/z/nodepkgs/marked`' package-lock.json \
|
||||
&& sed -ri 's`https://registry.npmjs.org/marked/-/marked-[0-9\.]+.tgz`file:/z/nodepkgs/marked`' package-lock.json \
|
||||
&& sed -ri 's`("marked": ")[^"]+`\1file:/z/nodepkgs/marked`' ./package.json \
|
||||
&& sed -ri 's`("codemirror": ")[^"]+`\1file:/z/nodepkgs/codemirror`' ./package.json \
|
||||
&& sed -ri 's`^var marked = require\(.marked/lib/marked.\);$`var marked = window.marked;`' src/js/easymde.js \
|
||||
&& npm install
|
||||
|
||||
COPY easymde-ln.patch /z/
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
diff --git a/src/Lexer.js b/src/Lexer.js
|
||||
adds linetracking to marked.js v1.0.0 +git;
|
||||
adds linetracking to marked.js v3.0.4;
|
||||
add data-ln="%d" to most tags, %d is the source markdown line
|
||||
--- a/src/Lexer.js
|
||||
+++ b/src/Lexer.js
|
||||
@@ -49,4 +49,5 @@ function mangle(text) {
|
||||
@@ -50,4 +50,5 @@ function mangle(text) {
|
||||
module.exports = class Lexer {
|
||||
constructor(options) {
|
||||
+ this.ln = 1; // like most editors, start couting from 1
|
||||
this.tokens = [];
|
||||
this.tokens.links = Object.create(null);
|
||||
@@ -108,4 +109,15 @@ module.exports = class Lexer {
|
||||
@@ -127,4 +128,15 @@ module.exports = class Lexer {
|
||||
}
|
||||
|
||||
+ set_ln(token, ln = this.ln) {
|
||||
@@ -25,122 +25,123 @@ add data-ln="%d" to most tags, %d is the source markdown line
|
||||
+
|
||||
/**
|
||||
* Lexing
|
||||
@@ -113,10 +125,15 @@ module.exports = class Lexer {
|
||||
blockTokens(src, tokens = [], top = true) {
|
||||
src = src.replace(/^ +$/gm, '');
|
||||
- let token, i, l, lastToken;
|
||||
+ let token, i, l, lastToken, ln;
|
||||
@@ -134,7 +146,11 @@ module.exports = class Lexer {
|
||||
src = src.replace(/^ +$/gm, '');
|
||||
}
|
||||
- let token, lastToken, cutSrc, lastParagraphClipped;
|
||||
+ let token, lastToken, cutSrc, lastParagraphClipped, ln;
|
||||
|
||||
while (src) {
|
||||
+ // this.ln will be bumped by recursive calls into this func;
|
||||
+ // reset the count and rely on the outermost token's raw only
|
||||
+ ln = this.ln;
|
||||
+
|
||||
// newline
|
||||
if (this.options.extensions
|
||||
&& this.options.extensions.block
|
||||
@@ -142,4 +158,5 @@ module.exports = class Lexer {
|
||||
if (token = extTokenizer.call({ lexer: this }, src, tokens)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
return true;
|
||||
@@ -153,4 +170,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.space(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token); // is \n if not type
|
||||
+ this.set_ln(token, ln); // is \n if not type
|
||||
if (token.type) {
|
||||
tokens.push(token);
|
||||
@@ -128,4 +145,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.code(src, tokens)) {
|
||||
@@ -162,4 +180,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.code(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
if (token.type) {
|
||||
tokens.push(token);
|
||||
@@ -141,4 +159,5 @@ module.exports = class Lexer {
|
||||
+ this.set_ln(token, ln);
|
||||
lastToken = tokens[tokens.length - 1];
|
||||
// An indented code block cannot interrupt a paragraph.
|
||||
@@ -177,4 +196,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.fences(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -148,4 +167,5 @@ module.exports = class Lexer {
|
||||
@@ -184,4 +204,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.heading(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -155,4 +175,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.nptable(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -162,4 +183,5 @@ module.exports = class Lexer {
|
||||
@@ -191,4 +212,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.hr(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -170,4 +192,7 @@ module.exports = class Lexer {
|
||||
@@ -198,4 +220,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.blockquote(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
token.tokens = this.blockTokens(token.text, [], top);
|
||||
+ // recursive call to blockTokens probably bumped this.ln,
|
||||
+ // token.raw is more reliable so reset this.ln and use that
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -180,5 +205,9 @@ module.exports = class Lexer {
|
||||
for (i = 0; i < l; i++) {
|
||||
token.items[i].tokens = this.blockTokens(token.items[i].text, [], false);
|
||||
+ // list entries don't bump the linecounter, so let's
|
||||
+ this.ln++;
|
||||
}
|
||||
+ // then reset like blockquote
|
||||
@@ -205,4 +228,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.list(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -188,4 +217,5 @@ module.exports = class Lexer {
|
||||
@@ -212,4 +236,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.html(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -195,4 +225,5 @@ module.exports = class Lexer {
|
||||
if (top && (token = this.tokenizer.def(src))) {
|
||||
@@ -219,4 +244,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.def(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
if (!this.tokens.links[token.tag]) {
|
||||
this.tokens.links[token.tag] = {
|
||||
@@ -207,4 +238,5 @@ module.exports = class Lexer {
|
||||
+ this.set_ln(token, ln);
|
||||
lastToken = tokens[tokens.length - 1];
|
||||
if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) {
|
||||
@@ -236,4 +262,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.table(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -214,4 +246,5 @@ module.exports = class Lexer {
|
||||
@@ -243,4 +270,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.lheading(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
+ this.set_ln(token, ln);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -221,4 +254,5 @@ module.exports = class Lexer {
|
||||
if (top && (token = this.tokenizer.paragraph(src))) {
|
||||
@@ -263,4 +291,5 @@ module.exports = class Lexer {
|
||||
}
|
||||
if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) {
|
||||
+ this.set_ln(token, ln);
|
||||
lastToken = tokens[tokens.length - 1];
|
||||
if (lastParagraphClipped && lastToken.type === 'paragraph') {
|
||||
@@ -280,4 +309,6 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.text(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -228,4 +262,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.text(src, tokens)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.set_ln(token);
|
||||
if (token.type) {
|
||||
tokens.push(token);
|
||||
@@ -263,4 +298,7 @@ module.exports = class Lexer {
|
||||
for (i = 0; i < l; i++) {
|
||||
token = tokens[i];
|
||||
+ // this.ln is at EOF when inline() is invoked;
|
||||
+ // all this affects <br> tags only so no biggie if it breaks
|
||||
+ this.ln = token.ln || this.ln;
|
||||
switch (token.type) {
|
||||
case 'paragraph':
|
||||
@@ -386,4 +424,6 @@ module.exports = class Lexer {
|
||||
+ this.set_ln(token, ln);
|
||||
+ this.ln++;
|
||||
lastToken = tokens[tokens.length - 1];
|
||||
if (lastToken && lastToken.type === 'text') {
|
||||
@@ -355,4 +386,5 @@ module.exports = class Lexer {
|
||||
if (token = extTokenizer.call({ lexer: this }, src, tokens)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.ln = token.ln || this.ln;
|
||||
tokens.push(token);
|
||||
return true;
|
||||
@@ -420,4 +452,6 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.br(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ // no need to reset (no more blockTokens anyways)
|
||||
+ token.ln = this.ln++;
|
||||
tokens.push(token);
|
||||
continue;
|
||||
@@ -462,4 +496,5 @@ module.exports = class Lexer {
|
||||
if (token = this.tokenizer.inlineText(cutSrc, smartypants)) {
|
||||
src = src.substring(token.raw.length);
|
||||
+ this.ln = token.ln || this.ln;
|
||||
if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started
|
||||
prevChar = token.raw.slice(-1);
|
||||
diff --git a/src/Parser.js b/src/Parser.js
|
||||
--- a/src/Parser.js
|
||||
+++ b/src/Parser.js
|
||||
@@ -150,17 +151,16 @@ diff --git a/src/Parser.js b/src/Parser.js
|
||||
+ this.ln = 0; // error indicator; should always be set >=1 from tokens
|
||||
}
|
||||
|
||||
@@ -55,4 +56,9 @@ module.exports = class Parser {
|
||||
@@ -64,4 +65,8 @@ module.exports = class Parser {
|
||||
for (i = 0; i < l; i++) {
|
||||
token = tokens[i];
|
||||
+ // take line-numbers from tokens whenever possible
|
||||
+ // and update the renderer's html attribute with the new value
|
||||
+ this.ln = token.ln || this.ln;
|
||||
+ this.renderer.tag_ln(this.ln);
|
||||
+
|
||||
switch (token.type) {
|
||||
case 'space': {
|
||||
@@ -105,7 +111,10 @@ module.exports = class Parser {
|
||||
|
||||
// Run any renderer extensions
|
||||
@@ -124,7 +129,10 @@ module.exports = class Parser {
|
||||
}
|
||||
|
||||
- body += this.renderer.tablerow(cell);
|
||||
@@ -173,7 +173,7 @@ diff --git a/src/Parser.js b/src/Parser.js
|
||||
+ out += this.renderer.tag_ln(token.ln).table(header, body);
|
||||
continue;
|
||||
}
|
||||
@@ -148,8 +157,12 @@ module.exports = class Parser {
|
||||
@@ -167,8 +175,12 @@ module.exports = class Parser {
|
||||
|
||||
itemBody += this.parse(item.tokens, loose);
|
||||
- body += this.renderer.listitem(itemBody, task, checked);
|
||||
@@ -188,7 +188,7 @@ diff --git a/src/Parser.js b/src/Parser.js
|
||||
+ out += this.renderer.tag_ln(token.ln).list(body, ordered, start);
|
||||
continue;
|
||||
}
|
||||
@@ -160,5 +173,6 @@ module.exports = class Parser {
|
||||
@@ -179,5 +191,6 @@ module.exports = class Parser {
|
||||
}
|
||||
case 'paragraph': {
|
||||
- out += this.renderer.paragraph(this.parseInline(token.tokens));
|
||||
@@ -196,22 +196,14 @@ diff --git a/src/Parser.js b/src/Parser.js
|
||||
+ out += this.renderer.tag_ln(token.ln).paragraph(t);
|
||||
continue;
|
||||
}
|
||||
@@ -199,4 +213,6 @@ module.exports = class Parser {
|
||||
for (i = 0; i < l; i++) {
|
||||
@@ -221,4 +234,7 @@ module.exports = class Parser {
|
||||
token = tokens[i];
|
||||
|
||||
+ // another thing that only affects <br/> and other inlines
|
||||
+ this.ln = token.ln || this.ln;
|
||||
switch (token.type) {
|
||||
case 'escape': {
|
||||
@@ -229,5 +245,7 @@ module.exports = class Parser {
|
||||
}
|
||||
case 'br': {
|
||||
- out += renderer.br();
|
||||
+ // update the html attribute before writing each <br/>,
|
||||
+ // don't care about the others
|
||||
+ out += renderer.tag_ln(this.ln).br();
|
||||
break;
|
||||
}
|
||||
+
|
||||
// Run any renderer extensions
|
||||
if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) {
|
||||
diff --git a/src/Renderer.js b/src/Renderer.js
|
||||
--- a/src/Renderer.js
|
||||
+++ b/src/Renderer.js
|
||||
@@ -228,7 +220,7 @@ diff --git a/src/Renderer.js b/src/Renderer.js
|
||||
+
|
||||
code(code, infostring, escaped) {
|
||||
const lang = (infostring || '').match(/\S*/)[0];
|
||||
@@ -24,10 +30,10 @@ module.exports = class Renderer {
|
||||
@@ -26,10 +32,10 @@ module.exports = class Renderer {
|
||||
|
||||
if (!lang) {
|
||||
- return '<pre><code>'
|
||||
@@ -241,58 +233,69 @@ diff --git a/src/Renderer.js b/src/Renderer.js
|
||||
+ return '<pre' + this.ln + '><code class="'
|
||||
+ this.options.langPrefix
|
||||
+ escape(lang, true)
|
||||
@@ -38,5 +44,5 @@ module.exports = class Renderer {
|
||||
@@ -40,5 +46,5 @@ module.exports = class Renderer {
|
||||
|
||||
blockquote(quote) {
|
||||
- return '<blockquote>\n' + quote + '</blockquote>\n';
|
||||
+ return '<blockquote' + this.ln + '>\n' + quote + '</blockquote>\n';
|
||||
}
|
||||
|
||||
@@ -49,4 +55,5 @@ module.exports = class Renderer {
|
||||
@@ -51,4 +57,5 @@ module.exports = class Renderer {
|
||||
return '<h'
|
||||
+ level
|
||||
+ + this.ln
|
||||
+ ' id="'
|
||||
+ this.options.headerPrefix
|
||||
@@ -59,5 +66,5 @@ module.exports = class Renderer {
|
||||
@@ -61,5 +68,5 @@ module.exports = class Renderer {
|
||||
}
|
||||
// ignore IDs
|
||||
- return '<h' + level + '>' + text + '</h' + level + '>\n';
|
||||
+ return '<h' + level + this.ln + '>' + text + '</h' + level + '>\n';
|
||||
}
|
||||
|
||||
@@ -73,5 +80,5 @@ module.exports = class Renderer {
|
||||
@@ -75,5 +82,5 @@ module.exports = class Renderer {
|
||||
|
||||
listitem(text) {
|
||||
- return '<li>' + text + '</li>\n';
|
||||
+ return '<li' + this.ln + '>' + text + '</li>\n';
|
||||
}
|
||||
|
||||
@@ -85,5 +92,5 @@ module.exports = class Renderer {
|
||||
@@ -87,5 +94,5 @@ module.exports = class Renderer {
|
||||
|
||||
paragraph(text) {
|
||||
- return '<p>' + text + '</p>\n';
|
||||
+ return '<p' + this.ln + '>' + text + '</p>\n';
|
||||
}
|
||||
|
||||
@@ -100,5 +107,5 @@ module.exports = class Renderer {
|
||||
@@ -102,5 +109,5 @@ module.exports = class Renderer {
|
||||
|
||||
tablerow(content) {
|
||||
- return '<tr>\n' + content + '</tr>\n';
|
||||
+ return '<tr' + this.ln + '>\n' + content + '</tr>\n';
|
||||
}
|
||||
|
||||
@@ -125,5 +132,5 @@ module.exports = class Renderer {
|
||||
@@ -127,5 +134,5 @@ module.exports = class Renderer {
|
||||
|
||||
br() {
|
||||
- return this.options.xhtml ? '<br/>' : '<br>';
|
||||
+ return this.options.xhtml ? '<br' + this.ln + '/>' : '<br' + this.ln + '>';
|
||||
}
|
||||
|
||||
@@ -151,5 +158,5 @@ module.exports = class Renderer {
|
||||
@@ -153,5 +160,5 @@ module.exports = class Renderer {
|
||||
}
|
||||
|
||||
- let out = '<img src="' + href + '" alt="' + text + '"';
|
||||
+ let out = '<img' + this.ln + ' src="' + href + '" alt="' + text + '"';
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||
--- a/src/Tokenizer.js
|
||||
+++ b/src/Tokenizer.js
|
||||
@@ -301,4 +301,7 @@ module.exports = class Tokenizer {
|
||||
const l = list.items.length;
|
||||
|
||||
+ // each nested list gets +1 ahead; this hack makes every listgroup -1 but atleast it doesn't get infinitely bad
|
||||
+ this.lexer.ln--;
|
||||
+
|
||||
// Item child tokens handled here at end because we needed to have the final item to trim it first
|
||||
for (i = 0; i < l; i++) {
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
diff --git a/src/Lexer.js b/src/Lexer.js
|
||||
--- a/src/Lexer.js
|
||||
+++ b/src/Lexer.js
|
||||
@@ -5,5 +5,5 @@ const { block, inline } = require('./rules.js');
|
||||
@@ -6,5 +6,5 @@ const { repeatString } = require('./helpers.js');
|
||||
/**
|
||||
* smartypants text replacement
|
||||
- */
|
||||
+ *
|
||||
function smartypants(text) {
|
||||
return text
|
||||
@@ -26,5 +26,5 @@ function smartypants(text) {
|
||||
@@ -27,5 +27,5 @@ function smartypants(text) {
|
||||
/**
|
||||
* mangle email addresses
|
||||
- */
|
||||
+ *
|
||||
function mangle(text) {
|
||||
let out = '',
|
||||
@@ -439,5 +439,5 @@ module.exports = class Lexer {
|
||||
@@ -465,5 +465,5 @@ module.exports = class Lexer {
|
||||
|
||||
// autolink
|
||||
- if (token = this.tokenizer.autolink(src, mangle)) {
|
||||
+ if (token = this.tokenizer.autolink(src)) {
|
||||
src = src.substring(token.raw.length);
|
||||
tokens.push(token);
|
||||
@@ -446,5 +446,5 @@ module.exports = class Lexer {
|
||||
@@ -472,5 +472,5 @@ module.exports = class Lexer {
|
||||
|
||||
// url (gfm)
|
||||
- if (!inLink && (token = this.tokenizer.url(src, mangle))) {
|
||||
+ if (!inLink && (token = this.tokenizer.url(src))) {
|
||||
- if (!this.state.inLink && (token = this.tokenizer.url(src, mangle))) {
|
||||
+ if (!this.state.inLink && (token = this.tokenizer.url(src))) {
|
||||
src = src.substring(token.raw.length);
|
||||
tokens.push(token);
|
||||
@@ -453,5 +453,5 @@ module.exports = class Lexer {
|
||||
|
||||
// text
|
||||
- if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
|
||||
+ if (token = this.tokenizer.inlineText(src, inRawBlock)) {
|
||||
@@ -493,5 +493,5 @@ module.exports = class Lexer {
|
||||
}
|
||||
}
|
||||
- if (token = this.tokenizer.inlineText(cutSrc, smartypants)) {
|
||||
+ if (token = this.tokenizer.inlineText(cutSrc)) {
|
||||
src = src.substring(token.raw.length);
|
||||
tokens.push(token);
|
||||
this.ln = token.ln || this.ln;
|
||||
diff --git a/src/Renderer.js b/src/Renderer.js
|
||||
--- a/src/Renderer.js
|
||||
+++ b/src/Renderer.js
|
||||
@@ -140,5 +140,5 @@ module.exports = class Renderer {
|
||||
@@ -142,5 +142,5 @@ module.exports = class Renderer {
|
||||
|
||||
link(href, title, text) {
|
||||
- href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
||||
+ href = cleanUrl(this.options.baseUrl, href);
|
||||
if (href === null) {
|
||||
return text;
|
||||
@@ -153,5 +153,5 @@ module.exports = class Renderer {
|
||||
@@ -155,5 +155,5 @@ module.exports = class Renderer {
|
||||
|
||||
image(href, title, text) {
|
||||
- href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
||||
@@ -56,22 +56,23 @@ diff --git a/src/Renderer.js b/src/Renderer.js
|
||||
diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||
--- a/src/Tokenizer.js
|
||||
+++ b/src/Tokenizer.js
|
||||
@@ -287,11 +287,8 @@ module.exports = class Tokenizer {
|
||||
if (cap) {
|
||||
return {
|
||||
- type: this.options.sanitize
|
||||
- ? 'paragraph'
|
||||
- : 'html',
|
||||
+ type: 'html',
|
||||
@@ -321,14 +321,7 @@ module.exports = class Tokenizer {
|
||||
type: 'html',
|
||||
raw: cap[0],
|
||||
- pre: !this.options.sanitizer
|
||||
- && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
|
||||
- text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]
|
||||
+ pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',
|
||||
+ text: cap[0]
|
||||
+ pre: (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
|
||||
text: cap[0]
|
||||
};
|
||||
- if (this.options.sanitize) {
|
||||
- token.type = 'paragraph';
|
||||
- token.text = this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]);
|
||||
- token.tokens = [];
|
||||
- this.lexer.inline(token.text, token.tokens);
|
||||
- }
|
||||
return token;
|
||||
}
|
||||
@@ -421,15 +418,9 @@ module.exports = class Tokenizer {
|
||||
@@ -477,15 +470,9 @@ module.exports = class Tokenizer {
|
||||
|
||||
return {
|
||||
- type: this.options.sanitize
|
||||
@@ -79,8 +80,8 @@ diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||
- : 'html',
|
||||
+ type: 'html',
|
||||
raw: cap[0],
|
||||
inLink,
|
||||
inRawBlock,
|
||||
inLink: this.lexer.state.inLink,
|
||||
inRawBlock: this.lexer.state.inRawBlock,
|
||||
- text: this.options.sanitize
|
||||
- ? (this.options.sanitizer
|
||||
- ? this.options.sanitizer(cap[0])
|
||||
@@ -89,7 +90,7 @@ diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||
+ text: cap[0]
|
||||
};
|
||||
}
|
||||
@@ -550,10 +541,10 @@ module.exports = class Tokenizer {
|
||||
@@ -672,10 +659,10 @@ module.exports = class Tokenizer {
|
||||
}
|
||||
|
||||
- autolink(src, mangle) {
|
||||
@@ -102,7 +103,7 @@ diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||
+ text = escape(cap[1]);
|
||||
href = 'mailto:' + text;
|
||||
} else {
|
||||
@@ -578,10 +569,10 @@ module.exports = class Tokenizer {
|
||||
@@ -700,10 +687,10 @@ module.exports = class Tokenizer {
|
||||
}
|
||||
|
||||
- url(src, mangle) {
|
||||
@@ -115,15 +116,15 @@ diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||
+ text = escape(cap[0]);
|
||||
href = 'mailto:' + text;
|
||||
} else {
|
||||
@@ -615,12 +606,12 @@ module.exports = class Tokenizer {
|
||||
@@ -737,12 +724,12 @@ module.exports = class Tokenizer {
|
||||
}
|
||||
|
||||
- inlineText(src, inRawBlock, smartypants) {
|
||||
+ inlineText(src, inRawBlock) {
|
||||
- inlineText(src, smartypants) {
|
||||
+ inlineText(src) {
|
||||
const cap = this.rules.inline.text.exec(src);
|
||||
if (cap) {
|
||||
let text;
|
||||
if (inRawBlock) {
|
||||
if (this.lexer.state.inRawBlock) {
|
||||
- text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0];
|
||||
+ text = cap[0];
|
||||
} else {
|
||||
@@ -134,7 +135,7 @@ diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||
diff --git a/src/defaults.js b/src/defaults.js
|
||||
--- a/src/defaults.js
|
||||
+++ b/src/defaults.js
|
||||
@@ -8,12 +8,8 @@ function getDefaults() {
|
||||
@@ -9,12 +9,8 @@ function getDefaults() {
|
||||
highlight: null,
|
||||
langPrefix: 'language-',
|
||||
- mangle: true,
|
||||
@@ -170,7 +171,7 @@ diff --git a/src/helpers.js b/src/helpers.js
|
||||
+function cleanUrl(base, href) {
|
||||
if (base && !originIndependentUrl.test(href)) {
|
||||
href = resolveUrl(base, href);
|
||||
@@ -223,10 +210,4 @@ function findClosingBracket(str, b) {
|
||||
@@ -227,10 +214,4 @@ function findClosingBracket(str, b) {
|
||||
}
|
||||
|
||||
-function checkSanitizeDeprecation(opt) {
|
||||
@@ -179,14 +180,13 @@ diff --git a/src/helpers.js b/src/helpers.js
|
||||
- }
|
||||
-}
|
||||
-
|
||||
module.exports = {
|
||||
escape,
|
||||
@@ -239,5 +220,4 @@ module.exports = {
|
||||
splitCells,
|
||||
// copied from https://stackoverflow.com/a/5450113/806777
|
||||
function repeatString(pattern, count) {
|
||||
@@ -260,5 +241,4 @@ module.exports = {
|
||||
rtrim,
|
||||
- findClosingBracket,
|
||||
- checkSanitizeDeprecation
|
||||
+ findClosingBracket
|
||||
findClosingBracket,
|
||||
- checkSanitizeDeprecation,
|
||||
repeatString
|
||||
};
|
||||
diff --git a/src/marked.js b/src/marked.js
|
||||
--- a/src/marked.js
|
||||
@@ -203,8 +203,14 @@ diff --git a/src/marked.js b/src/marked.js
|
||||
- checkSanitizeDeprecation(opt);
|
||||
|
||||
if (callback) {
|
||||
@@ -108,5 +106,5 @@ function marked(src, opt, callback) {
|
||||
return Parser.parse(tokens, opt);
|
||||
@@ -302,5 +300,4 @@ marked.parseInline = function(src, opt) {
|
||||
|
||||
opt = merge({}, marked.defaults, opt || {});
|
||||
- checkSanitizeDeprecation(opt);
|
||||
|
||||
try {
|
||||
@@ -311,5 +308,5 @@ marked.parseInline = function(src, opt) {
|
||||
return Parser.parseInline(tokens, opt);
|
||||
} catch (e) {
|
||||
- e.message += '\nPlease report this to https://github.com/markedjs/marked.';
|
||||
+ e.message += '\nmake issue @ https://github.com/9001/copyparty';
|
||||
@@ -252,86 +258,87 @@ diff --git a/test/bench.js b/test/bench.js
|
||||
diff --git a/test/specs/run-spec.js b/test/specs/run-spec.js
|
||||
--- a/test/specs/run-spec.js
|
||||
+++ b/test/specs/run-spec.js
|
||||
@@ -22,8 +22,4 @@ function runSpecs(title, dir, showCompletionTable, options) {
|
||||
@@ -22,9 +22,4 @@ function runSpecs(title, dir, showCompletionTable, options) {
|
||||
}
|
||||
|
||||
- if (spec.options.sanitizer) {
|
||||
- // eslint-disable-next-line no-eval
|
||||
- spec.options.sanitizer = eval(spec.options.sanitizer);
|
||||
- }
|
||||
|
||||
-
|
||||
(spec.only ? fit : (spec.skip ? xit : it))('should ' + passFail + example, async() => {
|
||||
@@ -53,3 +49,2 @@ runSpecs('Original', './original', false, { gfm: false, pedantic: true });
|
||||
const before = process.hrtime();
|
||||
@@ -53,3 +48,2 @@ runSpecs('Original', './original', false, { gfm: false, pedantic: true });
|
||||
runSpecs('New', './new');
|
||||
runSpecs('ReDOS', './redos');
|
||||
-runSpecs('Security', './security', false, { silent: true }); // silent - do not show deprecation warning
|
||||
diff --git a/test/unit/Lexer-spec.js b/test/unit/Lexer-spec.js
|
||||
--- a/test/unit/Lexer-spec.js
|
||||
+++ b/test/unit/Lexer-spec.js
|
||||
@@ -465,5 +465,5 @@ a | b
|
||||
@@ -589,5 +589,5 @@ paragraph
|
||||
});
|
||||
|
||||
- it('sanitize', () => {
|
||||
+ /*it('sanitize', () => {
|
||||
expectTokens({
|
||||
md: '<div>html</div>',
|
||||
@@ -483,5 +483,5 @@ a | b
|
||||
@@ -607,5 +607,5 @@ paragraph
|
||||
]
|
||||
});
|
||||
- });
|
||||
+ });*/
|
||||
});
|
||||
|
||||
@@ -587,5 +587,5 @@ a | b
|
||||
@@ -652,5 +652,5 @@ paragraph
|
||||
});
|
||||
|
||||
- it('html sanitize', () => {
|
||||
+ /*it('html sanitize', () => {
|
||||
expectInlineTokens({
|
||||
md: '<div>html</div>',
|
||||
@@ -597,5 +597,5 @@ a | b
|
||||
@@ -660,5 +660,5 @@ paragraph
|
||||
]
|
||||
});
|
||||
- });
|
||||
+ });*/
|
||||
|
||||
it('link', () => {
|
||||
@@ -909,5 +909,5 @@ a | b
|
||||
@@ -971,5 +971,5 @@ paragraph
|
||||
});
|
||||
|
||||
- it('autolink mangle email', () => {
|
||||
+ /*it('autolink mangle email', () => {
|
||||
expectInlineTokens({
|
||||
md: '<test@example.com>',
|
||||
@@ -929,5 +929,5 @@ a | b
|
||||
@@ -991,5 +991,5 @@ paragraph
|
||||
]
|
||||
});
|
||||
- });
|
||||
+ });*/
|
||||
|
||||
it('url', () => {
|
||||
@@ -966,5 +966,5 @@ a | b
|
||||
@@ -1028,5 +1028,5 @@ paragraph
|
||||
});
|
||||
|
||||
- it('url mangle email', () => {
|
||||
+ /*it('url mangle email', () => {
|
||||
expectInlineTokens({
|
||||
md: 'test@example.com',
|
||||
@@ -986,5 +986,5 @@ a | b
|
||||
@@ -1048,5 +1048,5 @@ paragraph
|
||||
]
|
||||
});
|
||||
- });
|
||||
+ });*/
|
||||
});
|
||||
|
||||
@@ -1002,5 +1002,5 @@ a | b
|
||||
@@ -1064,5 +1064,5 @@ paragraph
|
||||
});
|
||||
|
||||
- describe('smartypants', () => {
|
||||
+ /*describe('smartypants', () => {
|
||||
it('single quotes', () => {
|
||||
expectInlineTokens({
|
||||
@@ -1072,5 +1072,5 @@ a | b
|
||||
@@ -1134,5 +1134,5 @@ paragraph
|
||||
});
|
||||
});
|
||||
- });
|
||||
|
||||
@@ -16,14 +16,14 @@ help() { exec cat <<'EOF'
|
||||
#
|
||||
# `no-sh` makes just the python sfx, skips the sh/unix sfx
|
||||
#
|
||||
# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
|
||||
# `no-ogv` saves ~192k by removing the opus/vorbis audio codecs
|
||||
# (only affects apple devices; everything else has native support)
|
||||
#
|
||||
# `no-cm` saves ~90k by removing easymde/codemirror
|
||||
# `no-cm` saves ~92k by removing easymde/codemirror
|
||||
# (the fancy markdown editor)
|
||||
#
|
||||
# `no-fnt` saves ~9k by removing the source-code-pro font
|
||||
# (mainly used my the markdown viewer/editor)
|
||||
# (browsers will try to use 'Consolas' instead)
|
||||
#
|
||||
# `no-dd` saves ~2k by removing the mouse cursor
|
||||
|
||||
@@ -37,6 +37,8 @@ gtar=$(command -v gtar || command -v gnutar) || true
|
||||
sed() { gsed "$@"; }
|
||||
find() { gfind "$@"; }
|
||||
sort() { gsort "$@"; }
|
||||
shuf() { gshuf "$@"; }
|
||||
nproc() { gnproc; }
|
||||
sha1sum() { shasum "$@"; }
|
||||
unexpand() { gunexpand "$@"; }
|
||||
command -v grealpath >/dev/null &&
|
||||
@@ -65,6 +67,7 @@ pybin=$(command -v python3 || command -v python) || {
|
||||
use_gz=
|
||||
do_sh=1
|
||||
do_py=1
|
||||
zopf=2560
|
||||
while [ ! -z "$1" ]; do
|
||||
case $1 in
|
||||
clean) clean=1 ; ;;
|
||||
@@ -76,6 +79,7 @@ while [ ! -z "$1" ]; do
|
||||
no-cm) no_cm=1 ; ;;
|
||||
no-sh) do_sh= ; ;;
|
||||
no-py) do_py= ; ;;
|
||||
fast) zopf=100 ; ;;
|
||||
*) help ; ;;
|
||||
esac
|
||||
shift
|
||||
@@ -134,7 +138,7 @@ tmpdir="$(
|
||||
# msys2 tar is bad, make the best of it
|
||||
echo collecting source
|
||||
[ $clean ] && {
|
||||
(cd .. && git archive master >tar) && tar -xf ../tar copyparty
|
||||
(cd .. && git archive hovudstraum >tar) && tar -xf ../tar copyparty
|
||||
(cd .. && tar -cf tar copyparty/web/deps) && tar -xf ../tar
|
||||
}
|
||||
[ $clean ] || {
|
||||
@@ -144,6 +148,7 @@ tmpdir="$(
|
||||
}
|
||||
|
||||
ver=
|
||||
[ -z "$repack" ] &&
|
||||
git describe --tags >/dev/null 2>/dev/null && {
|
||||
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
|
||||
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//')";
|
||||
@@ -175,7 +180,7 @@ git describe --tags >/dev/null 2>/dev/null && {
|
||||
|
||||
[ -z "$ver" ] &&
|
||||
ver="$(awk '/^VERSION *= \(/ {
|
||||
gsub(/[^0-9,]/,""); gsub(/,/,"."); print; exit}' < copyparty/__version__.py)"
|
||||
gsub(/[^0-9,a-g-]/,""); gsub(/,/,"."); print; exit}' < copyparty/__version__.py)"
|
||||
|
||||
ts=$(date -u +%s)
|
||||
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)
|
||||
@@ -201,6 +206,15 @@ while IFS= read -r x; do
|
||||
tmv "$x"
|
||||
done
|
||||
|
||||
find copyparty | LC_ALL=C sort | sed 's/\.gz$//;s/$/,/' > have
|
||||
cat have | while IFS= read -r x; do
|
||||
grep -qF -- "$x" ../scripts/sfx.ls || {
|
||||
echo "unexpected file: $x"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
rm have
|
||||
|
||||
[ $no_ogv ] &&
|
||||
rm -rf copyparty/web/deps/{dynamicaudio,ogv}*
|
||||
|
||||
@@ -214,17 +228,17 @@ done
|
||||
|
||||
[ $no_fnt ] && {
|
||||
rm -f copyparty/web/deps/scp.woff2
|
||||
f=copyparty/web/md.css
|
||||
gzip -d "$f"
|
||||
sed -r '/scp\.woff2/d' <$f >t
|
||||
f=copyparty/web/ui.css
|
||||
gzip -d "$f.gz" || true
|
||||
sed -r "s/src:.*scp.*\)/src:local('Consolas')/" <$f >t
|
||||
tmv "$f"
|
||||
}
|
||||
|
||||
[ $no_dd ] && {
|
||||
rm -rf copyparty/web/dd
|
||||
f=copyparty/web/browser.css
|
||||
gzip -d "$f"
|
||||
sed -r 's/(cursor: )url\([^)]+\), (pointer)/\1\2/; /[0-9]+% \{cursor:/d; /animation: cursor/d' <$f >t
|
||||
gzip -d "$f.gz" || true
|
||||
sed -r 's/(cursor: ?)url\([^)]+\), ?(pointer)/\1\2/; /[0-9]+% \{cursor:/d; /animation: ?cursor/d' <$f >t
|
||||
tmv "$f"
|
||||
}
|
||||
|
||||
@@ -238,6 +252,12 @@ f=dep-j2/jinja2/constants.py
|
||||
awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/{o=0}' <$f >t
|
||||
tmv "$f"
|
||||
|
||||
grep -rLE '^#[^a-z]*coding: utf-8' dep-j2 |
|
||||
while IFS= read -r f; do
|
||||
(echo "# coding: utf-8"; cat "$f") >t
|
||||
tmv "$f"
|
||||
done
|
||||
|
||||
# up2k goes from 28k to 22k laff
|
||||
awk 'BEGIN{gensub(//,"",1)}' </dev/null &&
|
||||
echo entabbening &&
|
||||
@@ -262,14 +282,27 @@ done
|
||||
|
||||
gzres() {
|
||||
command -v pigz &&
|
||||
pk='pigz -11 -I 256' ||
|
||||
pk="pigz -11 -I $zopf" ||
|
||||
pk='gzip'
|
||||
|
||||
echo "$pk"
|
||||
find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do
|
||||
np=$(nproc)
|
||||
echo "$pk #$np"
|
||||
|
||||
while IFS=' ' read -r _ f; do
|
||||
while true; do
|
||||
na=$(ps auxwww | grep -F "$pk" | wc -l)
|
||||
[ $na -le $np ] && break
|
||||
sleep 0.2
|
||||
done
|
||||
echo -n .
|
||||
$pk "$f"
|
||||
done
|
||||
$pk "$f" &
|
||||
done < <(
|
||||
find -printf '%s %p\n' |
|
||||
grep -E '\.(js|css)$' |
|
||||
grep -vF /deps/ |
|
||||
sort -nr
|
||||
)
|
||||
wait
|
||||
echo
|
||||
}
|
||||
|
||||
@@ -303,7 +336,7 @@ for d in copyparty dep-j2; do find $d -type f; done |
|
||||
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
||||
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
||||
|
||||
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1) >list || true
|
||||
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | shuf) >list || true
|
||||
|
||||
echo creating tar
|
||||
args=(--owner=1000 --group=1000)
|
||||
|
||||
@@ -61,7 +61,7 @@ rls_dir="$tmp/copyparty-$ver"
|
||||
mkdir "$rls_dir"
|
||||
|
||||
echo ">>> export from git"
|
||||
git archive master | tar -xC "$rls_dir"
|
||||
git archive hovudstraum | tar -xC "$rls_dir"
|
||||
|
||||
echo ">>> export untracked deps"
|
||||
tar -c copyparty/web/deps | tar -xC "$rls_dir"
|
||||
@@ -122,5 +122,5 @@ echo " $zip_path"
|
||||
echo " $tgz_path"
|
||||
echo
|
||||
|
||||
# function alr() { ls -alR copyparty-$1 | sed -r "s/copyparty-$1/copyparty/" | sed -r 's/[A-Z][a-z]{2} [0-9 ]{2} [0-9]{2}:[0-9]{2}//' > $1; }; for x in master rls src ; do alr $x; done
|
||||
# function alr() { ls -alR copyparty-$1 | sed -r "s/copyparty-$1/copyparty/" | sed -r 's/[A-Z][a-z]{2} [0-9 ]{2} [0-9]{2}:[0-9]{2}//' > $1; }; for x in hovudstraum rls src ; do alr $x; done
|
||||
|
||||
|
||||
36
scripts/rls.sh
Executable file
36
scripts/rls.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd ~/dev/copyparty/scripts
|
||||
|
||||
v=$1
|
||||
printf '%s\n' "$v" | grep -qE '^[0-9\.]+$' || exit 1
|
||||
grep -E "(${v//./, })" ../copyparty/__version__.py || exit 1
|
||||
|
||||
git tag v$v
|
||||
git push origin --tags
|
||||
|
||||
rm -rf ../dist
|
||||
|
||||
./make-pypi-release.sh u
|
||||
(cd .. && python3 ./setup.py clean2)
|
||||
|
||||
./make-tgz-release.sh $v
|
||||
|
||||
rm -f ../dist/copyparty-sfx.*
|
||||
./make-sfx.sh no-sh
|
||||
../dist/copyparty-sfx.py -h
|
||||
|
||||
ar=
|
||||
while true; do
|
||||
for ((a=0; a<100; a++)); do
|
||||
for f in ../dist/copyparty-sfx.{py,sh}; do
|
||||
[ -e $f ] || continue;
|
||||
mv $f $f.$(wc -c <$f | awk '{print$1}')
|
||||
done
|
||||
./make-sfx.sh re $ar
|
||||
done
|
||||
ar=no-sh
|
||||
done
|
||||
|
||||
# git tag -d v$v; git push --delete origin v$v
|
||||
77
scripts/sfx.ls
Normal file
77
scripts/sfx.ls
Normal file
@@ -0,0 +1,77 @@
|
||||
copyparty,
|
||||
copyparty/__init__.py,
|
||||
copyparty/__main__.py,
|
||||
copyparty/__version__.py,
|
||||
copyparty/authsrv.py,
|
||||
copyparty/bos,
|
||||
copyparty/bos/__init__.py,
|
||||
copyparty/bos/bos.py,
|
||||
copyparty/bos/path.py,
|
||||
copyparty/broker_mp.py,
|
||||
copyparty/broker_mpw.py,
|
||||
copyparty/broker_thr.py,
|
||||
copyparty/broker_util.py,
|
||||
copyparty/httpcli.py,
|
||||
copyparty/httpconn.py,
|
||||
copyparty/httpsrv.py,
|
||||
copyparty/ico.py,
|
||||
copyparty/mtag.py,
|
||||
copyparty/res,
|
||||
copyparty/res/insecure.pem,
|
||||
copyparty/star.py,
|
||||
copyparty/stolen,
|
||||
copyparty/stolen/__init__.py,
|
||||
copyparty/stolen/surrogateescape.py,
|
||||
copyparty/sutil.py,
|
||||
copyparty/svchub.py,
|
||||
copyparty/szip.py,
|
||||
copyparty/tcpsrv.py,
|
||||
copyparty/th_cli.py,
|
||||
copyparty/th_srv.py,
|
||||
copyparty/u2idx.py,
|
||||
copyparty/up2k.py,
|
||||
copyparty/util.py,
|
||||
copyparty/web,
|
||||
copyparty/web/baguettebox.js,
|
||||
copyparty/web/browser.css,
|
||||
copyparty/web/browser.html,
|
||||
copyparty/web/browser.js,
|
||||
copyparty/web/browser2.html,
|
||||
copyparty/web/copyparty.gif,
|
||||
copyparty/web/dd,
|
||||
copyparty/web/dd/2.png,
|
||||
copyparty/web/dd/3.png,
|
||||
copyparty/web/dd/4.png,
|
||||
copyparty/web/dd/5.png,
|
||||
copyparty/web/deps,
|
||||
copyparty/web/deps/easymde.css,
|
||||
copyparty/web/deps/easymde.js,
|
||||
copyparty/web/deps/marked.js,
|
||||
copyparty/web/deps/mini-fa.css,
|
||||
copyparty/web/deps/mini-fa.woff,
|
||||
copyparty/web/deps/ogv-decoder-audio-opus-wasm.js,
|
||||
copyparty/web/deps/ogv-decoder-audio-opus-wasm.wasm,
|
||||
copyparty/web/deps/ogv-decoder-audio-vorbis-wasm.js,
|
||||
copyparty/web/deps/ogv-decoder-audio-vorbis-wasm.wasm,
|
||||
copyparty/web/deps/ogv-demuxer-ogg-wasm.js,
|
||||
copyparty/web/deps/ogv-demuxer-ogg-wasm.wasm,
|
||||
copyparty/web/deps/ogv-worker-audio.js,
|
||||
copyparty/web/deps/ogv.js,
|
||||
copyparty/web/deps/scp.woff2,
|
||||
copyparty/web/deps/sha512.ac.js,
|
||||
copyparty/web/deps/sha512.hw.js,
|
||||
copyparty/web/md.css,
|
||||
copyparty/web/md.html,
|
||||
copyparty/web/md.js,
|
||||
copyparty/web/md2.css,
|
||||
copyparty/web/md2.js,
|
||||
copyparty/web/mde.css,
|
||||
copyparty/web/mde.html,
|
||||
copyparty/web/mde.js,
|
||||
copyparty/web/msg.css,
|
||||
copyparty/web/msg.html,
|
||||
copyparty/web/splash.css,
|
||||
copyparty/web/splash.html,
|
||||
copyparty/web/ui.css,
|
||||
copyparty/web/up2k.js,
|
||||
copyparty/web/util.js,
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# coding: latin-1
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -60,7 +60,7 @@ class Cpp(object):
|
||||
pass
|
||||
|
||||
|
||||
def tc1():
|
||||
def tc1(vflags):
|
||||
ub = "http://127.0.0.1:4321/"
|
||||
td = os.path.join("srv", "smoketest")
|
||||
try:
|
||||
@@ -100,17 +100,17 @@ def tc1():
|
||||
for d1 in ["r", "w", "a"]:
|
||||
pdirs.append("{}/{}".format(td, d1))
|
||||
pdirs.append("{}/{}/j".format(td, d1))
|
||||
for d2 in ["r", "w", "a"]:
|
||||
for d2 in ["r", "w", "a", "c"]:
|
||||
d = os.path.join(td, d1, "j", d2)
|
||||
pdirs.append(d)
|
||||
os.makedirs(d)
|
||||
|
||||
pdirs = [x.replace("\\", "/") for x in pdirs]
|
||||
udirs = [x.split("/", 2)[2] for x in pdirs]
|
||||
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
||||
perms = [x.rstrip("cj/")[-1] for x in pdirs]
|
||||
perms = ["rw" if x == "a" else x for x in perms]
|
||||
for pd, ud, p in zip(pdirs, udirs, perms):
|
||||
if ud[-1] == "j":
|
||||
if ud[-1] == "j" or ud[-1] == "c":
|
||||
continue
|
||||
|
||||
hp = None
|
||||
@@ -123,29 +123,37 @@ def tc1():
|
||||
hp = "-"
|
||||
hpaths[ud] = os.path.join(pd, ".hist")
|
||||
|
||||
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
||||
arg = "{}:{}:{}".format(pd, ud, p)
|
||||
if hp:
|
||||
arg += ":c,hist=" + hp
|
||||
|
||||
args += ["-v", arg]
|
||||
args += ["-v", arg + vflags]
|
||||
|
||||
# return
|
||||
cpp = Cpp(args)
|
||||
CPP.append(cpp)
|
||||
cpp.await_idle(ub, 3)
|
||||
|
||||
for d in udirs:
|
||||
for d, p in zip(udirs, perms):
|
||||
vid = ovid + "\n{}".format(d).encode("utf-8")
|
||||
try:
|
||||
requests.post(ub + d, data={"act": "bput"}, files={"f": ("a.h264", vid)})
|
||||
except:
|
||||
pass
|
||||
r = requests.post(
|
||||
ub + d,
|
||||
data={"act": "bput"},
|
||||
files={"f": (d.replace("/", "") + ".h264", vid)},
|
||||
)
|
||||
c = r.status_code
|
||||
if c == 200 and p not in ["w", "rw"]:
|
||||
raise Exception("post {} with perm {} at {}".format(c, p, d))
|
||||
elif c == 403 and p not in ["r"]:
|
||||
raise Exception("post {} with perm {} at {}".format(c, p, d))
|
||||
elif c not in [200, 403]:
|
||||
raise Exception("post {} with perm {} at {}".format(c, p, d))
|
||||
|
||||
cpp.clean()
|
||||
|
||||
# GET permission
|
||||
for d, p in zip(udirs, perms):
|
||||
u = "{}{}/a.h264".format(ub, d)
|
||||
u = "{}{}/{}.h264".format(ub, d, d.replace("/", ""))
|
||||
r = requests.get(u)
|
||||
ok = bool(r)
|
||||
if ok != (p in ["rw"]):
|
||||
@@ -153,14 +161,14 @@ def tc1():
|
||||
|
||||
# stat filesystem
|
||||
for d, p in zip(pdirs, perms):
|
||||
u = "{}/a.h264".format(d)
|
||||
u = "{}/{}.h264".format(d, d.split("test/")[-1].replace("/", ""))
|
||||
ok = os.path.exists(u)
|
||||
if ok != (p in ["rw", "w"]):
|
||||
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
||||
|
||||
# GET thumbnail, vreify contents
|
||||
for d, p in zip(udirs, perms):
|
||||
u = "{}{}/a.h264?th=j".format(ub, d)
|
||||
u = "{}{}/{}.h264?th=j".format(ub, d, d.replace("/", ""))
|
||||
r = requests.get(u)
|
||||
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
||||
if ok != (p in ["rw"]):
|
||||
@@ -192,9 +200,9 @@ def tc1():
|
||||
cpp.stop(True)
|
||||
|
||||
|
||||
def run(tc):
|
||||
def run(tc, *a):
|
||||
try:
|
||||
tc()
|
||||
tc(*a)
|
||||
finally:
|
||||
try:
|
||||
CPP[0].stop(False)
|
||||
@@ -203,7 +211,8 @@ def run(tc):
|
||||
|
||||
|
||||
def main():
|
||||
run(tc1)
|
||||
run(tc1, "")
|
||||
run(tc1, ":c,fk")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
@@ -8,7 +8,7 @@ import tokenize
|
||||
|
||||
|
||||
def uncomment(fpath):
|
||||
""" modified https://stackoverflow.com/a/62074206 """
|
||||
"""modified https://stackoverflow.com/a/62074206"""
|
||||
|
||||
with open(fpath, "rb") as f:
|
||||
orig = f.read().decode("utf-8")
|
||||
|
||||
7
setup.py
7
setup.py
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function
|
||||
|
||||
@@ -61,7 +61,7 @@ class clean2(Command):
|
||||
pass
|
||||
|
||||
nuke = []
|
||||
for (dirpath, dirnames, filenames) in os.walk("."):
|
||||
for (dirpath, _, filenames) in os.walk("."):
|
||||
for fn in filenames:
|
||||
if (
|
||||
fn.startswith("MANIFEST")
|
||||
@@ -86,7 +86,7 @@ args = {
|
||||
"url": "https://github.com/9001/copyparty",
|
||||
"license": "MIT",
|
||||
"classifiers": [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2",
|
||||
@@ -101,6 +101,7 @@ args = {
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: Jython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Environment :: Console",
|
||||
"Environment :: No Input/Output (Daemon)",
|
||||
|
||||
20
srv/test.md
20
srv/test.md
@@ -1,11 +1,17 @@
|
||||
### hello world
|
||||
|
||||
* qwe
|
||||
* asd
|
||||
* zxc
|
||||
* 573
|
||||
* one
|
||||
* two
|
||||
* rty
|
||||
* uio
|
||||
* asd
|
||||
* fgh
|
||||
* jkl
|
||||
* zxc
|
||||
* vbn
|
||||
* 573
|
||||
* one
|
||||
* two
|
||||
* three
|
||||
|
||||
* |||
|
||||
|--|--|
|
||||
@@ -134,12 +140,12 @@ a newline toplevel
|
||||
| a table | on the right |
|
||||
| second row | foo bar |
|
||||
|
||||
||
|
||||
a||a
|
||||
--|:-:|-:
|
||||
a table | big text in this | aaakbfddd
|
||||
second row | centred | bbb
|
||||
|
||||
||
|
||||
||||
|
||||
--|--|--
|
||||
foo
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
@@ -39,6 +39,8 @@ class Cfg(Namespace):
|
||||
no_scandir=False,
|
||||
no_sendfile=True,
|
||||
no_rescan=True,
|
||||
no_logues=False,
|
||||
no_readme=False,
|
||||
re_maxage=0,
|
||||
ihead=False,
|
||||
nih=True,
|
||||
@@ -96,7 +98,7 @@ class TestHttpCli(unittest.TestCase):
|
||||
if not vol.startswith(top):
|
||||
continue
|
||||
|
||||
mode = vol[-2].replace("a", "rwmd")
|
||||
mode = vol[-2].replace("a", "rw")
|
||||
usr = vol[-1]
|
||||
if usr == "a":
|
||||
usr = ""
|
||||
@@ -151,6 +153,7 @@ class TestHttpCli(unittest.TestCase):
|
||||
tar = tarfile.open(fileobj=io.BytesIO(b)).getnames()
|
||||
except:
|
||||
tar = []
|
||||
tar = [x[4:] if x.startswith("top/") else x for x in tar]
|
||||
tar = ["/".join([y for y in [top, durl, x] if y]) for x in tar]
|
||||
tar = [[x] + self.can_rw(x) for x in tar]
|
||||
tar_ok = [x[0] for x in tar if x[1]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
@@ -26,6 +26,8 @@ class Cfg(Namespace):
|
||||
"no_hash": False,
|
||||
"css_browser": None,
|
||||
"no_voldump": True,
|
||||
"no_logues": False,
|
||||
"no_readme": False,
|
||||
"re_maxage": 0,
|
||||
"rproxy": 0,
|
||||
}
|
||||
@@ -195,10 +197,10 @@ class TestVFS(unittest.TestCase):
|
||||
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||
self.assertAxs(n.axs.uread, ["*"])
|
||||
self.assertAxs(n.axs.uwrite, [])
|
||||
self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False])
|
||||
self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False])
|
||||
self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False])
|
||||
self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False])
|
||||
self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False, False])
|
||||
self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False, False])
|
||||
self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False, False])
|
||||
self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False, False])
|
||||
|
||||
# breadth-first construction
|
||||
vfs = AuthSrv(
|
||||
|
||||
Reference in New Issue
Block a user