mirror of
https://github.com/9001/copyparty.git
synced 2025-10-24 00:24:04 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf3941cf7a | ||
|
|
3649e8288a | ||
|
|
9a45e26026 | ||
|
|
e65f127571 | ||
|
|
3bfc699787 | ||
|
|
955318428a | ||
|
|
f6279b356a | ||
|
|
4cc3cdc989 | ||
|
|
f9aa20a3ad | ||
|
|
129d33f1a0 | ||
|
|
1ad7a3f378 | ||
|
|
b533be8818 | ||
|
|
fb729e5166 | ||
|
|
d337ecdb20 | ||
|
|
5f1f0a48b0 | ||
|
|
e0f1cb94a5 | ||
|
|
a362ee2246 | ||
|
|
19f23c686e | ||
|
|
23b20ff4a6 | ||
|
|
72574da834 | ||
|
|
d5a79455d1 | ||
|
|
070d4b9da9 | ||
|
|
0ace22fffe | ||
|
|
9e483d7694 | ||
|
|
26458b7a06 | ||
|
|
b6a4604952 | ||
|
|
af752fbbc2 | ||
|
|
279c9d706a | ||
|
|
806e7b5530 | ||
|
|
f3dc6a217b | ||
|
|
7671d791fa | ||
|
|
8cd84608a5 | ||
|
|
980c6fc810 | ||
|
|
fb40a484c5 | ||
|
|
daa9dedcaa | ||
|
|
0d634345ac | ||
|
|
e648252479 | ||
|
|
179d7a9ad8 | ||
|
|
19bc962ad5 | ||
|
|
27cce086c6 | ||
|
|
fec0c620d4 | ||
|
|
05a1a31cab | ||
|
|
d020527c6f | ||
|
|
4451485664 | ||
|
|
a4e1a3738a | ||
|
|
4339dbeb8d | ||
|
|
5b0605774c | ||
|
|
e3684e25f8 | ||
|
|
1359213196 | ||
|
|
03efc6a169 | ||
|
|
15b5982211 | ||
|
|
0eb3a5d387 | ||
|
|
7f8777389c | ||
|
|
4eb20f10ad | ||
|
|
daa11df558 | ||
|
|
1bb0db30a0 |
76
README.md
76
README.md
@@ -16,6 +16,11 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [thumbnails](#thumbnails) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
|
||||
|
||||
|
||||
## breaking changes \o/
|
||||
|
||||
this is the readme for v0.12 which has a different expression for volume permissions (`-v`); see [the v0.11.x readme](https://github.com/9001/copyparty/tree/15b59822112dda56cee576df30f331252fc62628#readme) for stuff regarding the [current stable release](https://github.com/9001/copyparty/releases/tag/v0.11.47)
|
||||
|
||||
|
||||
## readme toc
|
||||
|
||||
* top
|
||||
@@ -30,11 +35,12 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
* [the browser](#the-browser)
|
||||
* [tabs](#tabs)
|
||||
* [hotkeys](#hotkeys)
|
||||
* [tree-mode](#tree-mode)
|
||||
* [navpane](#navpane)
|
||||
* [thumbnails](#thumbnails)
|
||||
* [zip downloads](#zip-downloads)
|
||||
* [uploading](#uploading)
|
||||
* [file-search](#file-search)
|
||||
* [file manager](#file-manager)
|
||||
* [markdown viewer](#markdown-viewer)
|
||||
* [other tricks](#other-tricks)
|
||||
* [searching](#searching)
|
||||
@@ -66,15 +72,14 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
|
||||
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||
|
||||
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want accounts and volumes etc
|
||||
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want [accounts and volumes](#accounts-and-volumes) etc
|
||||
|
||||
some recommended options:
|
||||
* `-e2dsa` enables general file indexing, see [search configuration](#search-configuration)
|
||||
* `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen), see [optional dependencies](#optional-dependencies)
|
||||
* `-v /mnt/music:/music:r:afoo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, with user `foo` as `a`dmin (read/write), password `bar`
|
||||
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||
* replace `:r:afoo` with `:rfoo` to only make the folder readable by `foo` and nobody else
|
||||
* in addition to `r`ead and `a`dmin, `w`rite makes a folder write-only, so cannot list/access files in it
|
||||
* `-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)
|
||||
* `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access
|
||||
|
||||
you may also want these, especially on servers:
|
||||
@@ -117,7 +122,7 @@ summary: all planned features work! now please enjoy the bloatening
|
||||
* ☑ sanic multipart parser
|
||||
* ☑ multiprocessing (actual multithreading)
|
||||
* ☑ volumes (mountpoints)
|
||||
* ☑ accounts
|
||||
* ☑ [accounts](#accounts-and-volumes)
|
||||
* upload
|
||||
* ☑ basic: plain multipart, ie6 support
|
||||
* ☑ up2k: js, resumable, multithreaded
|
||||
@@ -128,7 +133,7 @@ summary: all planned features work! now please enjoy the bloatening
|
||||
* ☑ folders as zip / tar files
|
||||
* ☑ FUSE client (read-only)
|
||||
* browser
|
||||
* ☑ tree-view
|
||||
* ☑ navpane (directory tree sidebar)
|
||||
* ☑ audio player (with OS media controls)
|
||||
* ☑ thumbnails
|
||||
* ☑ ...of images using Pillow
|
||||
@@ -136,7 +141,7 @@ summary: all planned features work! now please enjoy the bloatening
|
||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ SPA (browse while uploading)
|
||||
* if you use the file-tree on the left only, not folders in the file list
|
||||
* if you use the navpane to navigate, not folders in the file list
|
||||
* server indexing
|
||||
* ☑ locate files by contents
|
||||
* ☑ search by name/path/date/size
|
||||
@@ -164,8 +169,6 @@ small collection of user feedback
|
||||
|
||||
* 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`
|
||||
* dupe files will not have metadata (audio tags etc) displayed in the file listing
|
||||
* because they don't get `up` entries in the db (probably best fix) and `tx_browser` does not `lstat`
|
||||
* probably more, pls let me know
|
||||
|
||||
## not my bugs
|
||||
@@ -180,6 +183,30 @@ small collection of user feedback
|
||||
* use `--hist` or the `hist` volflag (`-v [...]:chist=/tmp/foo`) to place the db inside the vm instead
|
||||
|
||||
|
||||
# accounts and volumes
|
||||
|
||||
* `-a usr:pwd` adds account `usr` with password `pwd`
|
||||
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
|
||||
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||
* when granting permissions to an account, the names are comma-separated: `-v .::r,usr1,usr2:rw,usr3,usr4`
|
||||
|
||||
permissions:
|
||||
* `r` (read): browse folder contents, download files, download as zip/tar
|
||||
* `w` (write): upload files, move files *into* folder
|
||||
* `m` (move): move files/folders *from* folder
|
||||
* `d` (delete): delete files/folders
|
||||
|
||||
example:
|
||||
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
||||
* make folder `/srv` the root of the filesystem, read-only by anyone: `-v /srv::r`
|
||||
* make folder `/mnt/music` available at `/music`, read-only for u1 and u2, read-write for u3: `-v /mnt/music:music:r,u1,u2:rw,u3`
|
||||
* unauthorized users accessing the webroot can see that the `music` folder exists, but cannot open it
|
||||
* make folder `/mnt/incoming` available at `/inc`, write-only for u1, read-move for u2: `-v /mnt/incoming:inc:w,u1:rm,u2`
|
||||
* unauthorized users accessing the webroot can see that the `inc` folder exists, but cannot open it
|
||||
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
|
||||
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
|
||||
|
||||
|
||||
# the browser
|
||||
|
||||

|
||||
@@ -199,11 +226,20 @@ small collection of user feedback
|
||||
## hotkeys
|
||||
|
||||
the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
||||
* `B` toggle breadcrumbs / directory tree
|
||||
* `B` toggle breadcrumbs / navpane
|
||||
* `I/K` prev/next folder
|
||||
* `M` parent folder (or unexpand current)
|
||||
* `G` toggle list / grid view
|
||||
* `T` toggle thumbnails / icons
|
||||
* `ctrl-X` cut selected files/folders
|
||||
* `ctrl-V` paste
|
||||
* `F2` rename selected file/folder
|
||||
* when a file/folder is selected (in not-grid-view):
|
||||
* `Up/Down` move cursor
|
||||
* shift+`Up/Down` select and move cursor
|
||||
* ctrl+`Up/Down` move cursor and scroll viewport
|
||||
* `Space` toggle file selection
|
||||
* `Ctrl-A` toggle select all
|
||||
* when playing audio:
|
||||
* `J/L` prev/next song
|
||||
* `U/O` skip 10sec back/forward
|
||||
@@ -220,7 +256,7 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
||||
* `C` continue playing next video
|
||||
* `R` loop
|
||||
* `M` mute
|
||||
* when tree-sidebar is open:
|
||||
* when the navpane is open:
|
||||
* `A/D` adjust tree width
|
||||
* in the grid view:
|
||||
* `S` toggle multiselect
|
||||
@@ -233,9 +269,10 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
||||
* `^e` toggle editor / preview
|
||||
* `^up, ^down` jump paragraphs
|
||||
|
||||
## tree-mode
|
||||
|
||||
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the `🌲` or pressing the `B` hotkey
|
||||
## navpane
|
||||
|
||||
by default there's a breadcrumbs path; you can replace this with a navpane (tree-browser sidebar thing) by clicking the `🌲` or pressing the `B` hotkey
|
||||
|
||||
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
||||
|
||||
@@ -325,6 +362,13 @@ note that since up2k has to read the file twice, `[🎈 bup]` can be up to 2x fa
|
||||
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)
|
||||
|
||||
|
||||
## file manager
|
||||
|
||||
if you have the required permissions, you can cut/paste, rename, and delete files/folders
|
||||
|
||||
you can move files across browser tabs (cut in one tab, paste in another)
|
||||
|
||||
|
||||
## markdown viewer
|
||||
|
||||

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