mirror of
https://github.com/9001/copyparty.git
synced 2025-11-06 06:43:53 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3dfd24c92 | ||
|
|
fa0a7f50bb | ||
|
|
44a78a7e21 | ||
|
|
6b75cbf747 | ||
|
|
e7b18ab9fe | ||
|
|
aa12830015 | ||
|
|
f156e00064 | ||
|
|
d53c212516 | ||
|
|
ca27f8587c | ||
|
|
88ce008e16 | ||
|
|
081d2cc5d7 | ||
|
|
60ac68d000 | ||
|
|
fbe656957d | ||
|
|
5534c78c17 | ||
|
|
a45a53fdce | ||
|
|
972a56e738 | ||
|
|
5e03b3ca38 | ||
|
|
1078d933b4 | ||
|
|
d6bf300d80 |
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -16,12 +16,9 @@
|
|||||||
"-e2ts",
|
"-e2ts",
|
||||||
"-mtp",
|
"-mtp",
|
||||||
".bpm=f,bin/mtag/audio-bpm.py",
|
".bpm=f,bin/mtag/audio-bpm.py",
|
||||||
"-a",
|
"-aed:wark",
|
||||||
"ed:wark",
|
"-vsrv::r:aed:cnodupe",
|
||||||
"-v",
|
"-vdist:dist:r"
|
||||||
"srv::r:aed:cnodupe",
|
|
||||||
"-v",
|
|
||||||
"dist:dist:r"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,5 +40,13 @@
|
|||||||
"${file}"
|
"${file}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Python: Current File",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
4
.vscode/launch.py
vendored
4
.vscode/launch.py
vendored
@@ -6,10 +6,14 @@
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
print(sys.executable)
|
||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
import jstyleson
|
import jstyleson
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
|
|
||||||
with open(".vscode/launch.json", "r", encoding="utf-8") as f:
|
with open(".vscode/launch.json", "r", encoding="utf-8") as f:
|
||||||
tj = f.read()
|
tj = f.read()
|
||||||
|
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -37,6 +37,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
* [other tricks](#other-tricks)
|
* [other tricks](#other-tricks)
|
||||||
* [searching](#searching)
|
* [searching](#searching)
|
||||||
* [search configuration](#search-configuration)
|
* [search configuration](#search-configuration)
|
||||||
|
* [database location](#database-location)
|
||||||
* [metadata from audio files](#metadata-from-audio-files)
|
* [metadata from audio files](#metadata-from-audio-files)
|
||||||
* [file parser plugins](#file-parser-plugins)
|
* [file parser plugins](#file-parser-plugins)
|
||||||
* [complete examples](#complete-examples)
|
* [complete examples](#complete-examples)
|
||||||
@@ -105,6 +106,7 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
* ☑ images using Pillow
|
* ☑ images using Pillow
|
||||||
* ☑ videos using FFmpeg
|
* ☑ videos using FFmpeg
|
||||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||||
|
* ☑ image gallery
|
||||||
* ☑ SPA (browse while uploading)
|
* ☑ SPA (browse while uploading)
|
||||||
* if you use the file-tree on the left only, not folders in the file list
|
* if you use the file-tree on the left only, not folders in the file list
|
||||||
* server indexing
|
* server indexing
|
||||||
@@ -121,12 +123,12 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
* Windows: python 3.7 and older cannot read tags with ffprobe, so use mutagen or upgrade
|
* Windows: python 3.7 and older cannot read tags with ffprobe, so use mutagen or upgrade
|
||||||
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
||||||
* Windows: python 2.7 cannot handle filenames with mojibake
|
* Windows: python 2.7 cannot handle filenames with mojibake
|
||||||
|
* MacOS: `--th-ff-jpg` may fix thumbnails using macports-FFmpeg
|
||||||
|
|
||||||
## general bugs
|
## general bugs
|
||||||
|
|
||||||
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
||||||
* cannot mount something at `/d1/d2/d3` unless `d2` exists inside `d1`
|
* cannot mount something at `/d1/d2/d3` unless `d2` exists inside `d1`
|
||||||
* hiding the contents at url `/d1/d2/d3` using `-v :d1/d2/d3:cd2d` has the side-effect of creating databases (for files/tags) inside folders d1 and d2, and those databases take precedence over the main db at the top of the vfs - this means all files in d2 and below will be reindexed unless you already had a vfs entry at or below d2
|
|
||||||
* probably more, pls let me know
|
* probably more, pls let me know
|
||||||
|
|
||||||
## not my bugs
|
## not my bugs
|
||||||
@@ -180,6 +182,8 @@ click `[-]` and `[+]` to adjust the size, and the `[a]` toggles if the tree shou
|
|||||||
|
|
||||||
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how destructive your users are
|
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how destructive your users are
|
||||||
|
|
||||||
|
images named `folder.jpg` and `folder.png` become the thumbnail of the folder they're in
|
||||||
|
|
||||||
|
|
||||||
## zip downloads
|
## zip downloads
|
||||||
|
|
||||||
@@ -296,9 +300,29 @@ the same arguments can be set as volume flags, in addition to `d2d` and `d2t` fo
|
|||||||
* `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on
|
* `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on
|
||||||
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||||
|
|
||||||
`e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and cause `e2ts` to reindex those
|
note:
|
||||||
|
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and cause `e2ts` to reindex those
|
||||||
|
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
||||||
|
|
||||||
the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `cdhash`, this has the following consequences:
|
||||||
|
* initial indexing is way faster, especially when the volume is on a networked disk
|
||||||
|
* makes it impossible to [file-search](#file-search)
|
||||||
|
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
||||||
|
|
||||||
|
if you set `--no-hash`, you can enable hashing for specific volumes using flag `cehash`
|
||||||
|
|
||||||
|
|
||||||
|
## database location
|
||||||
|
|
||||||
|
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
|
||||||
|
|
||||||
|
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both:
|
||||||
|
* `--hist ~/.cache/copyparty -v ~/music::r:chist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior)
|
||||||
|
|
||||||
|
note:
|
||||||
|
* markdown edits are always stored in a local `.hist` subdirectory
|
||||||
|
* on windows the volflag path is cyglike, so `/c/temp` means `C:\temp` but use regular paths for `--hist`
|
||||||
|
* you can use cygpaths for volumes too, `-v C:\Users::r` and `-v /c/users::r` both work
|
||||||
|
|
||||||
|
|
||||||
## metadata from audio files
|
## metadata from audio files
|
||||||
|
|||||||
@@ -263,6 +263,9 @@ def run_argparse(argv, formatter):
|
|||||||
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
|
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
|
||||||
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
|
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('appearance options')
|
||||||
|
ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('admin panel options')
|
ap2 = ap.add_argument_group('admin panel options')
|
||||||
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
|
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
|
||||||
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
|
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
|
||||||
@@ -274,6 +277,7 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
||||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||||
|
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs")
|
||||||
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown")
|
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown")
|
||||||
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval")
|
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval")
|
||||||
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
|
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
|
||||||
@@ -285,6 +289,8 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
|
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
|
||||||
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
|
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
|
||||||
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
||||||
|
ap2.add_argument("--hist", metavar="PATH", type=str, help="where to store volume state")
|
||||||
|
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||||
ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead")
|
ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead")
|
||||||
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
||||||
ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping")
|
ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping")
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 11, 11)
|
VERSION = (0, 11, 13)
|
||||||
CODENAME = "the grid"
|
CODENAME = "the grid"
|
||||||
BUILD_DT = (2021, 6, 8)
|
BUILD_DT = (2021, 6, 12)
|
||||||
|
|
||||||
S_VERSION = ".".join(map(str, VERSION))
|
S_VERSION = ".".join(map(str, VERSION))
|
||||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import re
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import stat
|
import stat
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .__init__ import WINDOWS
|
from .__init__ import WINDOWS
|
||||||
@@ -22,7 +24,15 @@ class VFS(object):
|
|||||||
self.uadm = uadm # users who are regular admins
|
self.uadm = uadm # users who are regular admins
|
||||||
self.flags = flags # config switches
|
self.flags = flags # config switches
|
||||||
self.nodes = {} # child nodes
|
self.nodes = {} # child nodes
|
||||||
self.all_vols = {vpath: self} if realpath else {} # flattened recursive
|
self.histtab = None # all realpath->histpath
|
||||||
|
self.dbv = None # closest full/non-jump parent
|
||||||
|
|
||||||
|
if realpath:
|
||||||
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||||
|
self.all_vols = {vpath: self} # flattened recursive
|
||||||
|
else:
|
||||||
|
self.histpath = None
|
||||||
|
self.all_vols = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "VFS({})".format(
|
return "VFS({})".format(
|
||||||
@@ -32,9 +42,12 @@ class VFS(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _trk(self, vol):
|
def get_all_vols(self, outdict):
|
||||||
self.all_vols[vol.vpath] = vol
|
if self.realpath:
|
||||||
return vol
|
outdict[self.vpath] = self
|
||||||
|
|
||||||
|
for v in self.nodes.values():
|
||||||
|
v.get_all_vols(outdict)
|
||||||
|
|
||||||
def add(self, src, dst):
|
def add(self, src, dst):
|
||||||
"""get existing, or add new path to the vfs"""
|
"""get existing, or add new path to the vfs"""
|
||||||
@@ -46,19 +59,19 @@ class VFS(object):
|
|||||||
name, dst = dst.split("/", 1)
|
name, dst = dst.split("/", 1)
|
||||||
if name in self.nodes:
|
if name in self.nodes:
|
||||||
# exists; do not manipulate permissions
|
# exists; do not manipulate permissions
|
||||||
return self._trk(self.nodes[name].add(src, dst))
|
return self.nodes[name].add(src, dst)
|
||||||
|
|
||||||
vn = VFS(
|
vn = VFS(
|
||||||
"{}/{}".format(self.realpath, name),
|
os.path.join(self.realpath, name) if self.realpath else None,
|
||||||
"{}/{}".format(self.vpath, name).lstrip("/"),
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
||||||
self.uread,
|
self.uread,
|
||||||
self.uwrite,
|
self.uwrite,
|
||||||
self.uadm,
|
self.uadm,
|
||||||
self.flags,
|
self._copy_flags(name),
|
||||||
)
|
)
|
||||||
self._trk(vn)
|
vn.dbv = self.dbv or self
|
||||||
self.nodes[name] = vn
|
self.nodes[name] = vn
|
||||||
return self._trk(vn.add(src, dst))
|
return vn.add(src, dst)
|
||||||
|
|
||||||
if dst in self.nodes:
|
if dst in self.nodes:
|
||||||
# leaf exists; return as-is
|
# leaf exists; return as-is
|
||||||
@@ -67,8 +80,26 @@ class VFS(object):
|
|||||||
# leaf does not exist; create and keep permissions blank
|
# leaf does not exist; create and keep permissions blank
|
||||||
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
||||||
vn = VFS(src, vp)
|
vn = VFS(src, vp)
|
||||||
|
vn.dbv = self.dbv or self
|
||||||
self.nodes[dst] = vn
|
self.nodes[dst] = vn
|
||||||
return self._trk(vn)
|
return vn
|
||||||
|
|
||||||
|
def _copy_flags(self, name):
|
||||||
|
flags = {k: v for k, v in self.flags.items()}
|
||||||
|
hist = flags.get("hist")
|
||||||
|
if hist and hist != "-":
|
||||||
|
flags["hist"] = "{}/{}".format(hist.rstrip("/"), name)
|
||||||
|
|
||||||
|
return flags
|
||||||
|
|
||||||
|
def bubble_flags(self):
|
||||||
|
if self.dbv:
|
||||||
|
for k, v in self.dbv.flags.items():
|
||||||
|
if k not in ["hist"]:
|
||||||
|
self.flags[k] = v
|
||||||
|
|
||||||
|
for v in self.nodes.values():
|
||||||
|
v.bubble_flags()
|
||||||
|
|
||||||
def _find(self, vpath):
|
def _find(self, vpath):
|
||||||
"""return [vfs,remainder]"""
|
"""return [vfs,remainder]"""
|
||||||
@@ -108,6 +139,15 @@ class VFS(object):
|
|||||||
|
|
||||||
return vn, rem
|
return vn, rem
|
||||||
|
|
||||||
|
def get_dbv(self, vrem):
|
||||||
|
dbv = self.dbv
|
||||||
|
if not dbv:
|
||||||
|
return self, vrem
|
||||||
|
|
||||||
|
vrem = [self.vpath[len(dbv.vpath) + 1 :], vrem]
|
||||||
|
vrem = "/".join([x for x in vrem if x])
|
||||||
|
return dbv, vrem
|
||||||
|
|
||||||
def canonical(self, rem):
|
def canonical(self, rem):
|
||||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||||
rp = self.realpath
|
rp = self.realpath
|
||||||
@@ -273,6 +313,7 @@ class AuthSrv(object):
|
|||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def log(self, msg, c=0):
|
def log(self, msg, c=0):
|
||||||
|
if self.log_func:
|
||||||
self.log_func("auth", msg, c)
|
self.log_func("auth", msg, c)
|
||||||
|
|
||||||
def laggy_iter(self, iterable):
|
def laggy_iter(self, iterable):
|
||||||
@@ -398,6 +439,9 @@ class AuthSrv(object):
|
|||||||
raise Exception("invalid -v argument: [{}]".format(v_str))
|
raise Exception("invalid -v argument: [{}]".format(v_str))
|
||||||
|
|
||||||
src, dst, perms = m.groups()
|
src, dst, perms = m.groups()
|
||||||
|
if WINDOWS and src.startswith("/"):
|
||||||
|
src = "{}:\\{}".format(src[1], src[3:])
|
||||||
|
|
||||||
# print("\n".join([src, dst, perms]))
|
# print("\n".join([src, dst, perms]))
|
||||||
src = fsdec(os.path.abspath(fsenc(src)))
|
src = fsdec(os.path.abspath(fsenc(src)))
|
||||||
dst = dst.strip("/")
|
dst = dst.strip("/")
|
||||||
@@ -451,6 +495,10 @@ class AuthSrv(object):
|
|||||||
v.uwrite = mwrite[dst]
|
v.uwrite = mwrite[dst]
|
||||||
v.uadm = madm[dst]
|
v.uadm = madm[dst]
|
||||||
v.flags = mflags[dst]
|
v.flags = mflags[dst]
|
||||||
|
v.dbv = None
|
||||||
|
|
||||||
|
vfs.all_vols = {}
|
||||||
|
vfs.get_all_vols(vfs.all_vols)
|
||||||
|
|
||||||
missing_users = {}
|
missing_users = {}
|
||||||
for d in [mread, mwrite]:
|
for d in [mread, mwrite]:
|
||||||
@@ -467,6 +515,69 @@ class AuthSrv(object):
|
|||||||
)
|
)
|
||||||
raise Exception("invalid config")
|
raise Exception("invalid config")
|
||||||
|
|
||||||
|
promote = []
|
||||||
|
demote = []
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
hid = hashlib.sha512(fsenc(vol.realpath)).digest()
|
||||||
|
hid = base64.b32encode(hid).decode("ascii").lower()
|
||||||
|
vflag = vol.flags.get("hist")
|
||||||
|
if vflag == "-":
|
||||||
|
pass
|
||||||
|
elif vflag:
|
||||||
|
if WINDOWS and vflag.startswith("/"):
|
||||||
|
vflag = "{}:\\{}".format(vflag[1], vflag[3:])
|
||||||
|
vol.histpath = vflag
|
||||||
|
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
|
||||||
|
|
||||||
|
powner = os.path.join(hpath, "owner.txt")
|
||||||
|
try:
|
||||||
|
with open(powner, "rb") as f:
|
||||||
|
owner = f.read().rstrip()
|
||||||
|
except:
|
||||||
|
owner = None
|
||||||
|
|
||||||
|
me = fsenc(vol.realpath).rstrip()
|
||||||
|
if owner not in [None, me]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if owner is None:
|
||||||
|
with open(powner, "wb") as f:
|
||||||
|
f.write(me)
|
||||||
|
|
||||||
|
vol.histpath = hpath
|
||||||
|
break
|
||||||
|
|
||||||
|
vol.histpath = os.path.realpath(vol.histpath)
|
||||||
|
if vol.dbv:
|
||||||
|
if os.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
||||||
|
promote.append(vol)
|
||||||
|
vol.dbv = None
|
||||||
|
else:
|
||||||
|
demote.append(vol)
|
||||||
|
|
||||||
|
# discard jump-vols
|
||||||
|
for v in demote:
|
||||||
|
vfs.all_vols.pop(v.vpath)
|
||||||
|
|
||||||
|
if promote:
|
||||||
|
msg = [
|
||||||
|
"\n the following jump-volumes were generated to assist the vfs.\n As they contain a database (probably from v0.11.11 or older),\n they are promoted to full volumes:"
|
||||||
|
]
|
||||||
|
for vol in promote:
|
||||||
|
msg.append(
|
||||||
|
" /{} ({}) ({})".format(vol.vpath, vol.realpath, vol.histpath)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log("\n\n".join(msg) + "\n", c=3)
|
||||||
|
|
||||||
|
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
||||||
|
|
||||||
all_mte = {}
|
all_mte = {}
|
||||||
errors = False
|
errors = False
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
@@ -476,6 +587,10 @@ class AuthSrv(object):
|
|||||||
if self.args.e2d or "e2ds" in vol.flags:
|
if self.args.e2d or "e2ds" in vol.flags:
|
||||||
vol.flags["e2d"] = True
|
vol.flags["e2d"] = True
|
||||||
|
|
||||||
|
if self.args.no_hash:
|
||||||
|
if "ehash" not in vol.flags:
|
||||||
|
vol.flags["dhash"] = True
|
||||||
|
|
||||||
for k in ["e2t", "e2ts", "e2tsr"]:
|
for k in ["e2t", "e2ts", "e2tsr"]:
|
||||||
if getattr(self.args, k):
|
if getattr(self.args, k):
|
||||||
vol.flags[k] = True
|
vol.flags[k] = True
|
||||||
@@ -553,6 +668,8 @@ class AuthSrv(object):
|
|||||||
if errors:
|
if errors:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
vfs.bubble_flags()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v, _ = vfs.get("/", "*", False, True)
|
v, _ = vfs.get("/", "*", False, True)
|
||||||
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
from copyparty.authsrv import AuthSrv
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -34,6 +35,9 @@ class MpWorker(object):
|
|||||||
if not FAKE_MP:
|
if not FAKE_MP:
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
|
||||||
|
# starting to look like a good idea
|
||||||
|
self.asrv = AuthSrv(args, None, False)
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.httpsrv = HttpSrv(self, True)
|
self.httpsrv = HttpSrv(self, True)
|
||||||
self.httpsrv.disconnect_func = self.httpdrop
|
self.httpsrv.disconnect_func = self.httpdrop
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from .authsrv import AuthSrv
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
from .broker_util import ExceptionalQueue, try_exec
|
from .broker_util import ExceptionalQueue, try_exec
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ class BrokerThr(object):
|
|||||||
self.hub = hub
|
self.hub = hub
|
||||||
self.log = hub.log
|
self.log = hub.log
|
||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
|
self.asrv = hub.asrv
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class HttpCli(object):
|
|||||||
self.addr = conn.addr # type: tuple[str, int]
|
self.addr = conn.addr # type: tuple[str, int]
|
||||||
self.args = conn.args
|
self.args = conn.args
|
||||||
self.is_mp = conn.is_mp
|
self.is_mp = conn.is_mp
|
||||||
self.auth = conn.auth # type: AuthSrv
|
self.asrv = conn.asrv # type: AuthSrv
|
||||||
self.ico = conn.ico
|
self.ico = conn.ico
|
||||||
self.thumbcli = conn.thumbcli
|
self.thumbcli = conn.thumbcli
|
||||||
self.log_func = conn.log_func
|
self.log_func = conn.log_func
|
||||||
@@ -154,9 +154,9 @@ class HttpCli(object):
|
|||||||
self.vpath = unquotep(vpath)
|
self.vpath = unquotep(vpath)
|
||||||
|
|
||||||
pwd = uparam.get("pw")
|
pwd = uparam.get("pw")
|
||||||
self.uname = self.auth.iuser.get(pwd, "*")
|
self.uname = self.asrv.iuser.get(pwd, "*")
|
||||||
self.rvol, self.wvol, self.avol = [[], [], []]
|
self.rvol, self.wvol, self.avol = [[], [], []]
|
||||||
self.auth.vfs.user_tree(self.uname, self.rvol, self.wvol, self.avol)
|
self.asrv.vfs.user_tree(self.uname, self.rvol, self.wvol, self.avol)
|
||||||
|
|
||||||
ua = self.headers.get("user-agent", "")
|
ua = self.headers.get("user-agent", "")
|
||||||
self.is_rclone = ua.startswith("rclone/")
|
self.is_rclone = ua.startswith("rclone/")
|
||||||
@@ -258,7 +258,14 @@ class HttpCli(object):
|
|||||||
return "?" + "&".join(r)
|
return "?" + "&".join(r)
|
||||||
|
|
||||||
def redirect(
|
def redirect(
|
||||||
self, vpath, suf="", msg="aight", flavor="go to", click=True, use302=False
|
self,
|
||||||
|
vpath,
|
||||||
|
suf="",
|
||||||
|
msg="aight",
|
||||||
|
flavor="go to",
|
||||||
|
click=True,
|
||||||
|
status=200,
|
||||||
|
use302=False,
|
||||||
):
|
):
|
||||||
html = self.j2(
|
html = self.j2(
|
||||||
"msg",
|
"msg",
|
||||||
@@ -273,7 +280,7 @@ class HttpCli(object):
|
|||||||
h = {"Location": "/" + vpath, "Cache-Control": "no-cache"}
|
h = {"Location": "/" + vpath, "Cache-Control": "no-cache"}
|
||||||
self.reply(html, status=302, headers=h)
|
self.reply(html, status=302, headers=h)
|
||||||
else:
|
else:
|
||||||
self.reply(html)
|
self.reply(html, status=status)
|
||||||
|
|
||||||
def handle_get(self):
|
def handle_get(self):
|
||||||
if self.do_log:
|
if self.do_log:
|
||||||
@@ -314,9 +321,7 @@ class HttpCli(object):
|
|||||||
self.redirect(vpath, flavor="redirecting to", use302=True)
|
self.redirect(vpath, flavor="redirecting to", use302=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.readable, self.writable = self.conn.auth.vfs.can_access(
|
self.readable, self.writable = self.asrv.vfs.can_access(self.vpath, self.uname)
|
||||||
self.vpath, self.uname
|
|
||||||
)
|
|
||||||
if not self.readable and not self.writable:
|
if not self.readable and not self.writable:
|
||||||
if self.vpath:
|
if self.vpath:
|
||||||
self.log("inaccessible: [{}]".format(self.vpath))
|
self.log("inaccessible: [{}]".format(self.vpath))
|
||||||
@@ -433,7 +438,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
def dump_to_file(self):
|
def dump_to_file(self):
|
||||||
reader, remains = self.get_body_reader()
|
reader, remains = self.get_body_reader()
|
||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
|
|
||||||
addr = self.ip.replace(":", ".")
|
addr = self.ip.replace(":", ".")
|
||||||
@@ -443,8 +448,10 @@ class HttpCli(object):
|
|||||||
with open(fsenc(path), "wb", 512 * 1024) as f:
|
with open(fsenc(path), "wb", 512 * 1024) as f:
|
||||||
post_sz, _, sha_b64 = hashcopy(self.conn, reader, f)
|
post_sz, _, sha_b64 = hashcopy(self.conn, reader, f)
|
||||||
|
|
||||||
|
vfs, vrem = vfs.get_dbv(rem)
|
||||||
|
|
||||||
self.conn.hsrv.broker.put(
|
self.conn.hsrv.broker.put(
|
||||||
False, "up2k.hash_file", vfs.realpath, vfs.flags, rem, fn
|
False, "up2k.hash_file", vfs.realpath, vfs.flags, vrem, fn
|
||||||
)
|
)
|
||||||
|
|
||||||
return post_sz, sha_b64, remains, path
|
return post_sz, sha_b64, remains, path
|
||||||
@@ -500,7 +507,7 @@ class HttpCli(object):
|
|||||||
if v is None:
|
if v is None:
|
||||||
raise Pebkac(422, "need zip or tar keyword")
|
raise Pebkac(422, "need zip or tar keyword")
|
||||||
|
|
||||||
vn, rem = self.auth.vfs.get(self.vpath, self.uname, True, False)
|
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, True, False)
|
||||||
items = self.parser.require("files", 1024 * 1024)
|
items = self.parser.require("files", 1024 * 1024)
|
||||||
if not items:
|
if not items:
|
||||||
raise Pebkac(422, "need files list")
|
raise Pebkac(422, "need files list")
|
||||||
@@ -550,13 +557,14 @@ class HttpCli(object):
|
|||||||
self.vpath = "/".join([self.vpath, sub]).strip("/")
|
self.vpath = "/".join([self.vpath, sub]).strip("/")
|
||||||
body["name"] = name
|
body["name"] = name
|
||||||
|
|
||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
|
dbv, vrem = vfs.get_dbv(rem)
|
||||||
|
|
||||||
body["vtop"] = vfs.vpath
|
body["vtop"] = dbv.vpath
|
||||||
body["ptop"] = vfs.realpath
|
body["ptop"] = dbv.realpath
|
||||||
body["prel"] = rem
|
body["prel"] = vrem
|
||||||
body["addr"] = self.ip
|
body["addr"] = self.ip
|
||||||
body["vcfg"] = vfs.flags
|
body["vcfg"] = dbv.flags
|
||||||
|
|
||||||
if sub:
|
if sub:
|
||||||
try:
|
try:
|
||||||
@@ -578,8 +586,14 @@ class HttpCli(object):
|
|||||||
|
|
||||||
def handle_search(self, body):
|
def handle_search(self, body):
|
||||||
vols = []
|
vols = []
|
||||||
|
seen = {}
|
||||||
for vtop in self.rvol:
|
for vtop in self.rvol:
|
||||||
vfs, _ = self.conn.auth.vfs.get(vtop, self.uname, True, False)
|
vfs, _ = self.asrv.vfs.get(vtop, self.uname, True, False)
|
||||||
|
vfs = vfs.dbv or vfs
|
||||||
|
if vfs in seen:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen[vfs] = True
|
||||||
vols.append([vfs.vpath, vfs.realpath, vfs.flags])
|
vols.append([vfs.vpath, vfs.realpath, vfs.flags])
|
||||||
|
|
||||||
idx = self.conn.get_u2idx()
|
idx = self.conn.get_u2idx()
|
||||||
@@ -635,8 +649,8 @@ class HttpCli(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise Pebkac(400, "need hash and wark headers for binary POST")
|
raise Pebkac(400, "need hash and wark headers for binary POST")
|
||||||
|
|
||||||
vfs, _ = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
ptop = vfs.realpath
|
ptop = (vfs.dbv or vfs).realpath
|
||||||
|
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.handle_chunk", ptop, wark, chash)
|
x = self.conn.hsrv.broker.put(True, "up2k.handle_chunk", ptop, wark, chash)
|
||||||
response = x.get()
|
response = x.get()
|
||||||
@@ -708,7 +722,7 @@ class HttpCli(object):
|
|||||||
pwd = self.parser.require("cppwd", 64)
|
pwd = self.parser.require("cppwd", 64)
|
||||||
self.parser.drop()
|
self.parser.drop()
|
||||||
|
|
||||||
if pwd in self.auth.iuser:
|
if pwd in self.asrv.iuser:
|
||||||
msg = "login ok"
|
msg = "login ok"
|
||||||
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
|
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
|
||||||
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||||
@@ -727,7 +741,7 @@ class HttpCli(object):
|
|||||||
self.parser.drop()
|
self.parser.drop()
|
||||||
|
|
||||||
nullwrite = self.args.nw
|
nullwrite = self.args.nw
|
||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
sanitized = sanitize_fn(new_dir)
|
sanitized = sanitize_fn(new_dir)
|
||||||
@@ -756,7 +770,7 @@ class HttpCli(object):
|
|||||||
self.parser.drop()
|
self.parser.drop()
|
||||||
|
|
||||||
nullwrite = self.args.nw
|
nullwrite = self.args.nw
|
||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
if not new_file.endswith(".md"):
|
if not new_file.endswith(".md"):
|
||||||
@@ -780,7 +794,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
def handle_plain_upload(self):
|
def handle_plain_upload(self):
|
||||||
nullwrite = self.args.nw
|
nullwrite = self.args.nw
|
||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
@@ -817,8 +831,14 @@ class HttpCli(object):
|
|||||||
raise Pebkac(400, "empty files in post")
|
raise Pebkac(400, "empty files in post")
|
||||||
|
|
||||||
files.append([sz, sha512_hex, p_file, fname])
|
files.append([sz, sha512_hex, p_file, fname])
|
||||||
|
dbv, vrem = vfs.get_dbv(rem)
|
||||||
self.conn.hsrv.broker.put(
|
self.conn.hsrv.broker.put(
|
||||||
False, "up2k.hash_file", vfs.realpath, vfs.flags, rem, fname
|
False,
|
||||||
|
"up2k.hash_file",
|
||||||
|
dbv.realpath,
|
||||||
|
dbv.flags,
|
||||||
|
vrem,
|
||||||
|
fname,
|
||||||
)
|
)
|
||||||
self.conn.nbyte += sz
|
self.conn.nbyte += sz
|
||||||
|
|
||||||
@@ -848,12 +868,16 @@ class HttpCli(object):
|
|||||||
status = "OK"
|
status = "OK"
|
||||||
if errmsg:
|
if errmsg:
|
||||||
self.log(errmsg)
|
self.log(errmsg)
|
||||||
errmsg = "ERROR: " + errmsg
|
|
||||||
status = "ERROR"
|
status = "ERROR"
|
||||||
|
|
||||||
msg = "{} // {} bytes // {:.3f} MiB/s\n".format(status, sz_total, spd)
|
msg = "{} // {} bytes // {:.3f} MiB/s\n".format(status, sz_total, spd)
|
||||||
jmsg = {"status": status, "sz": sz_total, "mbps": round(spd, 3), "files": []}
|
jmsg = {"status": status, "sz": sz_total, "mbps": round(spd, 3), "files": []}
|
||||||
|
|
||||||
|
if errmsg:
|
||||||
|
msg += errmsg + "\n"
|
||||||
|
jmsg["error"] = errmsg
|
||||||
|
errmsg = "ERROR: " + errmsg
|
||||||
|
|
||||||
for sz, sha512, ofn, lfn in files:
|
for sz, sha512, ofn, lfn in files:
|
||||||
vpath = (self.vpath + "/" if self.vpath else "") + lfn
|
vpath = (self.vpath + "/" if self.vpath else "") + lfn
|
||||||
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
|
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
|
||||||
@@ -885,11 +909,21 @@ class HttpCli(object):
|
|||||||
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
|
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
|
||||||
f.write(ft.encode("utf-8"))
|
f.write(ft.encode("utf-8"))
|
||||||
|
|
||||||
|
status = 400 if errmsg else 200
|
||||||
if "j" in self.uparam:
|
if "j" in self.uparam:
|
||||||
jtxt = json.dumps(jmsg, indent=2, sort_keys=True)
|
jtxt = json.dumps(jmsg, indent=2, sort_keys=True).encode("utf-8", "replace")
|
||||||
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
|
self.reply(jtxt, mime="application/json", status=status)
|
||||||
else:
|
else:
|
||||||
self.redirect(self.vpath, msg=msg, flavor="return to", click=False)
|
self.redirect(
|
||||||
|
self.vpath,
|
||||||
|
msg=msg,
|
||||||
|
flavor="return to",
|
||||||
|
click=False,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
if errmsg:
|
||||||
|
return False
|
||||||
|
|
||||||
self.parser.drop()
|
self.parser.drop()
|
||||||
return True
|
return True
|
||||||
@@ -901,7 +935,7 @@ class HttpCli(object):
|
|||||||
raise Pebkac(400, "could not read lastmod from request")
|
raise Pebkac(400, "could not read lastmod from request")
|
||||||
|
|
||||||
nullwrite = self.args.nw
|
nullwrite = self.args.nw
|
||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
@@ -1339,11 +1373,13 @@ class HttpCli(object):
|
|||||||
for y in [self.rvol, self.wvol, self.avol]
|
for y in [self.rvol, self.wvol, self.avol]
|
||||||
]
|
]
|
||||||
|
|
||||||
vstate = {}
|
|
||||||
if self.avol and not self.args.no_rescan:
|
if self.avol and not self.args.no_rescan:
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.get_volstate")
|
x = self.conn.hsrv.broker.put(True, "up2k.get_state")
|
||||||
vstate = json.loads(x.get())
|
vs = json.loads(x.get())
|
||||||
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vstate.items()}
|
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()}
|
||||||
|
else:
|
||||||
|
vstate = {}
|
||||||
|
vs = {"scanning": None, "hashq": None, "tagq": None, "mtpq": None}
|
||||||
|
|
||||||
html = self.j2(
|
html = self.j2(
|
||||||
"splash",
|
"splash",
|
||||||
@@ -1352,6 +1388,10 @@ class HttpCli(object):
|
|||||||
wvol=wvol,
|
wvol=wvol,
|
||||||
avol=avol,
|
avol=avol,
|
||||||
vstate=vstate,
|
vstate=vstate,
|
||||||
|
scanning=vs["scanning"],
|
||||||
|
hashq=vs["hashq"],
|
||||||
|
tagq=vs["tagq"],
|
||||||
|
mtpq=vs["mtpq"],
|
||||||
url_suf=suf,
|
url_suf=suf,
|
||||||
)
|
)
|
||||||
self.reply(html.encode("utf-8"), headers=NO_STORE)
|
self.reply(html.encode("utf-8"), headers=NO_STORE)
|
||||||
@@ -1364,9 +1404,10 @@ class HttpCli(object):
|
|||||||
if self.args.no_rescan:
|
if self.args.no_rescan:
|
||||||
raise Pebkac(403, "disabled by argv")
|
raise Pebkac(403, "disabled by argv")
|
||||||
|
|
||||||
vn, _ = self.auth.vfs.get(self.vpath, self.uname, True, True)
|
vn, _ = self.asrv.vfs.get(self.vpath, self.uname, True, True)
|
||||||
|
|
||||||
|
args = [self.asrv.vfs.all_vols, [vn.vpath]]
|
||||||
|
|
||||||
args = [self.auth.vfs.all_vols, [vn.vpath]]
|
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.rescan", *args)
|
x = self.conn.hsrv.broker.put(True, "up2k.rescan", *args)
|
||||||
x = x.get()
|
x = x.get()
|
||||||
if not x:
|
if not x:
|
||||||
@@ -1438,7 +1479,7 @@ class HttpCli(object):
|
|||||||
ret["k" + quotep(excl)] = sub
|
ret["k" + quotep(excl)] = sub
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vn, rem = self.auth.vfs.get(top, self.uname, True, False)
|
vn, rem = self.asrv.vfs.get(top, self.uname, True, False)
|
||||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||||
rem, self.uname, not self.args.no_scandir, incl_wo=True
|
rem, self.uname, not self.args.no_scandir, incl_wo=True
|
||||||
)
|
)
|
||||||
@@ -1479,31 +1520,47 @@ class HttpCli(object):
|
|||||||
|
|
||||||
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
||||||
|
|
||||||
vn, rem = self.auth.vfs.get(
|
vn, rem = self.asrv.vfs.get(
|
||||||
self.vpath, self.uname, self.readable, self.writable
|
self.vpath, self.uname, self.readable, self.writable
|
||||||
)
|
)
|
||||||
abspath = vn.canonical(rem)
|
abspath = vn.canonical(rem)
|
||||||
|
dbv, vrem = vn.get_dbv(rem)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = os.stat(fsenc(abspath))
|
st = os.stat(fsenc(abspath))
|
||||||
except:
|
except:
|
||||||
raise Pebkac(404)
|
raise Pebkac(404)
|
||||||
|
|
||||||
if self.readable and not stat.S_ISDIR(st.st_mode):
|
if self.readable:
|
||||||
if rem.startswith(".hist/up2k."):
|
if rem.startswith(".hist/up2k."):
|
||||||
raise Pebkac(403)
|
raise Pebkac(403)
|
||||||
|
|
||||||
|
is_dir = stat.S_ISDIR(st.st_mode)
|
||||||
th_fmt = self.uparam.get("th")
|
th_fmt = self.uparam.get("th")
|
||||||
if th_fmt is not None:
|
if th_fmt is not None:
|
||||||
|
if is_dir:
|
||||||
|
for fn in ["folder.png", "folder.jpg"]:
|
||||||
|
fp = os.path.join(abspath, fn)
|
||||||
|
if os.path.exists(fp):
|
||||||
|
vrem = "{}/{}".format(vrem.rstrip("/"), fn)
|
||||||
|
is_dir = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_dir:
|
||||||
|
return self.tx_ico("a.folder")
|
||||||
|
|
||||||
thp = None
|
thp = None
|
||||||
if self.thumbcli:
|
if self.thumbcli:
|
||||||
thp = self.thumbcli.get(vn.realpath, rem, int(st.st_mtime), th_fmt)
|
thp = self.thumbcli.get(
|
||||||
|
dbv.realpath, vrem, int(st.st_mtime), th_fmt
|
||||||
|
)
|
||||||
|
|
||||||
if thp:
|
if thp:
|
||||||
return self.tx_file(thp)
|
return self.tx_file(thp)
|
||||||
|
|
||||||
return self.tx_ico(rem)
|
return self.tx_ico(rem)
|
||||||
|
|
||||||
|
if not is_dir:
|
||||||
if abspath.endswith(".md") and "raw" not in self.uparam:
|
if abspath.endswith(".md") and "raw" not in self.uparam:
|
||||||
return self.tx_md(abspath)
|
return self.tx_md(abspath)
|
||||||
|
|
||||||
@@ -1640,7 +1697,7 @@ class HttpCli(object):
|
|||||||
icur = None
|
icur = None
|
||||||
if "e2t" in vn.flags:
|
if "e2t" in vn.flags:
|
||||||
idx = self.conn.get_u2idx()
|
idx = self.conn.get_u2idx()
|
||||||
icur = idx.get_cur(vn.realpath)
|
icur = idx.get_cur(dbv.realpath)
|
||||||
|
|
||||||
dirs = []
|
dirs = []
|
||||||
files = []
|
files = []
|
||||||
@@ -1708,6 +1765,9 @@ class HttpCli(object):
|
|||||||
rd = f["rd"]
|
rd = f["rd"]
|
||||||
del f["rd"]
|
del f["rd"]
|
||||||
if icur:
|
if icur:
|
||||||
|
if vn != dbv:
|
||||||
|
_, rd = vn.get_dbv(rd)
|
||||||
|
|
||||||
q = "select w from up where rd = ? and fn = ?"
|
q = "select w from up where rd = ? and fn = ?"
|
||||||
try:
|
try:
|
||||||
r = icur.execute(q, (rd, fn)).fetchone()
|
r = icur.execute(q, (rd, fn)).fetchone()
|
||||||
@@ -1748,9 +1808,13 @@ class HttpCli(object):
|
|||||||
j2a["files"] = dirs + files
|
j2a["files"] = dirs + files
|
||||||
j2a["logues"] = logues
|
j2a["logues"] = logues
|
||||||
j2a["taglist"] = taglist
|
j2a["taglist"] = taglist
|
||||||
|
|
||||||
if "mte" in vn.flags:
|
if "mte" in vn.flags:
|
||||||
j2a["tag_order"] = json.dumps(vn.flags["mte"].split(","))
|
j2a["tag_order"] = json.dumps(vn.flags["mte"].split(","))
|
||||||
|
|
||||||
|
if self.args.css_browser:
|
||||||
|
j2a["css"] = self.args.css_browser
|
||||||
|
|
||||||
html = self.j2(tpl, **j2a)
|
html = self.j2(tpl, **j2a)
|
||||||
self.reply(html.encode("utf-8", "replace"), headers=NO_STORE)
|
self.reply(html.encode("utf-8", "replace"), headers=NO_STORE)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class HttpConn(object):
|
|||||||
self.hsrv = hsrv
|
self.hsrv = hsrv
|
||||||
|
|
||||||
self.args = hsrv.args
|
self.args = hsrv.args
|
||||||
self.auth = hsrv.auth
|
self.asrv = hsrv.asrv
|
||||||
self.is_mp = hsrv.is_mp
|
self.is_mp = hsrv.is_mp
|
||||||
self.cert_path = hsrv.cert_path
|
self.cert_path = hsrv.cert_path
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class HttpConn(object):
|
|||||||
|
|
||||||
def get_u2idx(self):
|
def get_u2idx(self):
|
||||||
if not self.u2idx:
|
if not self.u2idx:
|
||||||
self.u2idx = U2idx(self.args, self.log_func)
|
self.u2idx = U2idx(self)
|
||||||
|
|
||||||
return self.u2idx
|
return self.u2idx
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class HttpSrv(object):
|
|||||||
self.is_mp = is_mp
|
self.is_mp = is_mp
|
||||||
self.args = broker.args
|
self.args = broker.args
|
||||||
self.log = broker.log
|
self.log = broker.log
|
||||||
|
self.asrv = broker.asrv
|
||||||
|
|
||||||
self.disconnect_func = None
|
self.disconnect_func = None
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
@@ -47,7 +48,6 @@ class HttpSrv(object):
|
|||||||
self.clients = {}
|
self.clients = {}
|
||||||
self.workload = 0
|
self.workload = 0
|
||||||
self.workload_thr_alive = False
|
self.workload_thr_alive = False
|
||||||
self.auth = AuthSrv(self.args, self.log)
|
|
||||||
|
|
||||||
env = jinja2.Environment()
|
env = jinja2.Environment()
|
||||||
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))
|
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -37,14 +37,13 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.log = self._log_disabled if args.q else self._log_enabled
|
self.log = self._log_disabled if args.q else self._log_enabled
|
||||||
|
|
||||||
# jank goes here
|
|
||||||
auth = AuthSrv(self.args, self.log, False)
|
|
||||||
if args.ls:
|
|
||||||
auth.dbg_ls()
|
|
||||||
|
|
||||||
# initiate all services to manage
|
# initiate all services to manage
|
||||||
|
self.asrv = AuthSrv(self.args, self.log, False)
|
||||||
|
if args.ls:
|
||||||
|
self.asrv.dbg_ls()
|
||||||
|
|
||||||
self.tcpsrv = TcpSrv(self)
|
self.tcpsrv = TcpSrv(self)
|
||||||
self.up2k = Up2k(self, auth.vfs.all_vols)
|
self.up2k = Up2k(self)
|
||||||
|
|
||||||
self.thumbsrv = None
|
self.thumbsrv = None
|
||||||
if not args.no_thumb:
|
if not args.no_thumb:
|
||||||
@@ -54,7 +53,7 @@ class SvcHub(object):
|
|||||||
msg = "setting --th-no-webp because either libwebp is not available or your Pillow is too old"
|
msg = "setting --th-no-webp because either libwebp is not available or your Pillow is too old"
|
||||||
self.log("thumb", msg, c=3)
|
self.log("thumb", msg, c=3)
|
||||||
|
|
||||||
self.thumbsrv = ThumbSrv(self, auth.vfs.all_vols)
|
self.thumbsrv = ThumbSrv(self)
|
||||||
else:
|
else:
|
||||||
msg = "need Pillow to create thumbnails; for example:\n{}{} -m pip install --user Pillow\n"
|
msg = "need Pillow to create thumbnails; for example:\n{}{} -m pip install --user Pillow\n"
|
||||||
self.log(
|
self.log(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
|
|
||||||
from .util import Cooldown
|
from .util import Cooldown
|
||||||
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
||||||
@@ -12,6 +11,7 @@ class ThumbCli(object):
|
|||||||
def __init__(self, broker):
|
def __init__(self, broker):
|
||||||
self.broker = broker
|
self.broker = broker
|
||||||
self.args = broker.args
|
self.args = broker.args
|
||||||
|
self.asrv = broker.asrv
|
||||||
|
|
||||||
# cache on both sides for less broker spam
|
# cache on both sides for less broker spam
|
||||||
self.cooldown = Cooldown(self.args.th_poke)
|
self.cooldown = Cooldown(self.args.th_poke)
|
||||||
@@ -21,16 +21,19 @@ class ThumbCli(object):
|
|||||||
if ext not in THUMBABLE:
|
if ext not in THUMBABLE:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self.args.no_vthumb and ext in FMT_FF:
|
is_vid = ext in FMT_FF
|
||||||
|
if is_vid and self.args.no_vthumb:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if fmt == "j" and self.args.th_no_jpg:
|
if fmt == "j" and self.args.th_no_jpg:
|
||||||
fmt = "w"
|
fmt = "w"
|
||||||
|
|
||||||
if fmt == "w" and self.args.th_no_webp:
|
if fmt == "w":
|
||||||
|
if self.args.th_no_webp or (is_vid and self.args.th_ff_jpg):
|
||||||
fmt = "j"
|
fmt = "j"
|
||||||
|
|
||||||
tpath = thumb_path(ptop, rem, mtime, fmt)
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
|
tpath = thumb_path(histpath, rem, mtime, fmt)
|
||||||
ret = None
|
ret = None
|
||||||
try:
|
try:
|
||||||
st = os.stat(tpath)
|
st = os.stat(tpath)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import shutil
|
import shutil
|
||||||
import base64
|
import base64
|
||||||
@@ -11,7 +10,7 @@ import threading
|
|||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
from .__init__ import PY2
|
from .__init__ import PY2
|
||||||
from .util import fsenc, mchkcmd, Queue, Cooldown, BytesIO
|
from .util import fsenc, runcmd, Queue, Cooldown, BytesIO, min_ex
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||||
|
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ except:
|
|||||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||||
# ffmpeg -formats
|
# ffmpeg -formats
|
||||||
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
|
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
|
||||||
FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
||||||
|
|
||||||
if HAVE_HEIF:
|
if HAVE_HEIF:
|
||||||
FMT_PIL += " heif heifs heic heics"
|
FMT_PIL += " heif heifs heic heics"
|
||||||
@@ -74,7 +73,7 @@ if HAVE_FFMPEG and HAVE_FFPROBE:
|
|||||||
THUMBABLE.update(FMT_FF)
|
THUMBABLE.update(FMT_FF)
|
||||||
|
|
||||||
|
|
||||||
def thumb_path(ptop, rem, mtime, fmt):
|
def thumb_path(histpath, rem, mtime, fmt):
|
||||||
# base16 = 16 = 256
|
# base16 = 16 = 256
|
||||||
# b64-lc = 38 = 1444
|
# b64-lc = 38 = 1444
|
||||||
# base64 = 64 = 4096
|
# base64 = 64 = 4096
|
||||||
@@ -95,16 +94,15 @@ def thumb_path(ptop, rem, mtime, fmt):
|
|||||||
h = hashlib.sha512(fsenc(fn)).digest()[:24]
|
h = hashlib.sha512(fsenc(fn)).digest()[:24]
|
||||||
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
|
|
||||||
return "{}/.hist/th/{}/{}.{:x}.{}".format(
|
return "{}/th/{}/{}.{:x}.{}".format(
|
||||||
ptop, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg"
|
histpath, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ThumbSrv(object):
|
class ThumbSrv(object):
|
||||||
def __init__(self, hub, vols):
|
def __init__(self, hub):
|
||||||
self.hub = hub
|
self.hub = hub
|
||||||
self.vols = [v.realpath for v in vols.values()]
|
self.asrv = hub.asrv
|
||||||
|
|
||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
@@ -153,7 +151,8 @@ class ThumbSrv(object):
|
|||||||
return not self.nthr
|
return not self.nthr
|
||||||
|
|
||||||
def get(self, ptop, rem, mtime, fmt):
|
def get(self, ptop, rem, mtime, fmt):
|
||||||
tpath = thumb_path(ptop, rem, mtime, fmt)
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
|
tpath = thumb_path(histpath, rem, mtime, fmt)
|
||||||
abspath = os.path.join(ptop, rem)
|
abspath = os.path.join(ptop, rem)
|
||||||
cond = threading.Condition()
|
cond = threading.Condition()
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
@@ -211,9 +210,9 @@ class ThumbSrv(object):
|
|||||||
if fun:
|
if fun:
|
||||||
try:
|
try:
|
||||||
fun(abspath, tpath)
|
fun(abspath, tpath)
|
||||||
except Exception as ex:
|
except:
|
||||||
msg = "{} failed on {}\n {!r}"
|
msg = "{} failed on {}\n{}"
|
||||||
self.log(msg.format(fun.__name__, abspath, ex), 3)
|
self.log(msg.format(fun.__name__, abspath, min_ex()), 3)
|
||||||
with open(tpath, "wb") as _:
|
with open(tpath, "wb") as _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -245,8 +244,8 @@ class ThumbSrv(object):
|
|||||||
except:
|
except:
|
||||||
im.thumbnail(self.res)
|
im.thumbnail(self.res)
|
||||||
|
|
||||||
if im.mode not in ("RGB", "L"):
|
fmts = ["RGB", "L"]
|
||||||
im = im.convert("RGB")
|
args = {"quality": 40}
|
||||||
|
|
||||||
if tpath.endswith(".webp"):
|
if tpath.endswith(".webp"):
|
||||||
# quality 80 = pillow-default
|
# quality 80 = pillow-default
|
||||||
@@ -254,15 +253,27 @@ class ThumbSrv(object):
|
|||||||
# method 0 = pillow-default, fast
|
# method 0 = pillow-default, fast
|
||||||
# method 4 = ffmpeg-default
|
# method 4 = ffmpeg-default
|
||||||
# method 6 = max, slow
|
# method 6 = max, slow
|
||||||
im.save(tpath, quality=40, method=6)
|
fmts += ["RGBA", "LA"]
|
||||||
|
args["method"] = 6
|
||||||
else:
|
else:
|
||||||
im.save(tpath, quality=40) # default=75
|
pass # default q = 75
|
||||||
|
|
||||||
|
if im.mode not in fmts:
|
||||||
|
print("conv {}".format(im.mode))
|
||||||
|
im = im.convert("RGB")
|
||||||
|
|
||||||
|
im.save(tpath, quality=40, method=6)
|
||||||
|
|
||||||
def conv_ffmpeg(self, abspath, tpath):
|
def conv_ffmpeg(self, abspath, tpath):
|
||||||
ret, _ = ffprobe(abspath)
|
ret, _ = ffprobe(abspath)
|
||||||
|
|
||||||
|
ext = abspath.rsplit(".")[-1]
|
||||||
|
if ext in ["h264", "h265"]:
|
||||||
|
seek = []
|
||||||
|
else:
|
||||||
dur = ret[".dur"][1] if ".dur" in ret else 4
|
dur = ret[".dur"][1] if ".dur" in ret else 4
|
||||||
seek = "{:.0f}".format(dur / 3)
|
seek = "{:.0f}".format(dur / 3)
|
||||||
|
seek = [b"-ss", seek.encode("utf-8")]
|
||||||
|
|
||||||
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
||||||
if self.args.th_no_crop:
|
if self.args.th_no_crop:
|
||||||
@@ -271,19 +282,20 @@ class ThumbSrv(object):
|
|||||||
scale += "increase,crop={0}:{1},setsar=1:1"
|
scale += "increase,crop={0}:{1},setsar=1:1"
|
||||||
|
|
||||||
scale = scale.format(*list(self.res)).encode("utf-8")
|
scale = scale.format(*list(self.res)).encode("utf-8")
|
||||||
|
# fmt: off
|
||||||
cmd = [
|
cmd = [
|
||||||
b"ffmpeg",
|
b"ffmpeg",
|
||||||
b"-nostdin",
|
b"-nostdin",
|
||||||
b"-hide_banner",
|
b"-v", b"error",
|
||||||
b"-ss",
|
b"-hide_banner"
|
||||||
seek,
|
|
||||||
b"-i",
|
|
||||||
fsenc(abspath),
|
|
||||||
b"-vf",
|
|
||||||
scale,
|
|
||||||
b"-vframes",
|
|
||||||
b"1",
|
|
||||||
]
|
]
|
||||||
|
cmd += seek
|
||||||
|
cmd += [
|
||||||
|
b"-i", fsenc(abspath),
|
||||||
|
b"-vf", scale,
|
||||||
|
b"-vframes", b"1",
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
if tpath.endswith(".jpg"):
|
if tpath.endswith(".jpg"):
|
||||||
cmd += [
|
cmd += [
|
||||||
@@ -300,7 +312,11 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
|
|
||||||
mchkcmd(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")
|
||||||
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
def poke(self, tdir):
|
def poke(self, tdir):
|
||||||
if not self.poke_cd.poke(tdir):
|
if not self.poke_cd.poke(tdir):
|
||||||
@@ -319,26 +335,29 @@ class ThumbSrv(object):
|
|||||||
interval = self.args.th_clean
|
interval = self.args.th_clean
|
||||||
while True:
|
while True:
|
||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
for vol in self.vols:
|
for vol, histpath in self.asrv.vfs.histtab.items():
|
||||||
vol += "/.hist/th"
|
if histpath.startswith(vol):
|
||||||
self.log("\033[Jcln {}/\033[A".format(vol))
|
self.log("\033[Jcln {}/\033[A".format(histpath))
|
||||||
self.clean(vol)
|
else:
|
||||||
|
self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol))
|
||||||
|
|
||||||
|
self.clean(histpath)
|
||||||
|
|
||||||
self.log("\033[Jcln ok")
|
self.log("\033[Jcln ok")
|
||||||
|
|
||||||
def clean(self, vol):
|
def clean(self, histpath):
|
||||||
# self.log("cln {}".format(vol))
|
# self.log("cln {}".format(histpath))
|
||||||
maxage = self.args.th_maxage
|
maxage = self.args.th_maxage
|
||||||
now = time.time()
|
now = time.time()
|
||||||
prev_b64 = None
|
prev_b64 = None
|
||||||
prev_fp = None
|
prev_fp = None
|
||||||
try:
|
try:
|
||||||
ents = os.listdir(vol)
|
ents = os.listdir(histpath)
|
||||||
except:
|
except:
|
||||||
return
|
return
|
||||||
|
|
||||||
for f in sorted(ents):
|
for f in sorted(ents):
|
||||||
fp = os.path.join(vol, f)
|
fp = os.path.join(histpath, f)
|
||||||
cmp = fp.lower().replace("\\", "/")
|
cmp = fp.lower().replace("\\", "/")
|
||||||
|
|
||||||
# "top" or b64 prefix/full (a folder)
|
# "top" or b64 prefix/full (a folder)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .util import u8safe, s3dec, html_escape, Pebkac
|
from .util import s3dec, Pebkac, min_ex
|
||||||
from .up2k import up2k_wark_from_hashlist
|
from .up2k import up2k_wark_from_hashlist
|
||||||
|
|
||||||
|
|
||||||
@@ -19,10 +19,11 @@ except:
|
|||||||
|
|
||||||
|
|
||||||
class U2idx(object):
|
class U2idx(object):
|
||||||
def __init__(self, args, log_func):
|
def __init__(self, conn):
|
||||||
self.args = args
|
self.log_func = conn.log_func
|
||||||
self.log_func = log_func
|
self.asrv = conn.asrv
|
||||||
self.timeout = args.srch_time
|
self.args = conn.args
|
||||||
|
self.timeout = self.args.srch_time
|
||||||
|
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
self.log("could not load sqlite3; searchign wqill be disabled")
|
self.log("could not load sqlite3; searchign wqill be disabled")
|
||||||
@@ -52,18 +53,20 @@ class U2idx(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, uq, uv)[0]
|
return self.run_query(vols, uq, uv)[0]
|
||||||
except Exception as ex:
|
except:
|
||||||
raise Pebkac(500, repr(ex))
|
raise Pebkac(500, min_ex())
|
||||||
|
|
||||||
def get_cur(self, ptop):
|
def get_cur(self, ptop):
|
||||||
cur = self.cur.get(ptop)
|
cur = self.cur.get(ptop)
|
||||||
if cur:
|
if cur:
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
cur = _open(ptop)
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
if not cur:
|
db_path = os.path.join(histpath, "up2k.db")
|
||||||
|
if not os.path.exists(db_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
cur = sqlite3.connect(db_path).cursor()
|
||||||
self.cur[ptop] = cur
|
self.cur[ptop] = cur
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
@@ -242,6 +245,7 @@ class U2idx(object):
|
|||||||
hit["tags"] = tags
|
hit["tags"] = tags
|
||||||
|
|
||||||
ret.extend(sret)
|
ret.extend(sret)
|
||||||
|
# print("[{}] {}".format(ptop, sret))
|
||||||
|
|
||||||
done_flag.append(True)
|
done_flag.append(True)
|
||||||
self.active_id = None
|
self.active_id = None
|
||||||
@@ -262,9 +266,3 @@ class U2idx(object):
|
|||||||
|
|
||||||
if identifier == self.active_id:
|
if identifier == self.active_id:
|
||||||
self.active_cur.connection.interrupt()
|
self.active_cur.connection.interrupt()
|
||||||
|
|
||||||
|
|
||||||
def _open(ptop):
|
|
||||||
db_path = os.path.join(ptop, ".hist", "up2k.db")
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
return sqlite3.connect(db_path).cursor()
|
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ class Up2k(object):
|
|||||||
* ~/.config flatfiles for active jobs
|
* ~/.config flatfiles for active jobs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hub, all_vols):
|
def __init__(self, hub):
|
||||||
self.hub = hub
|
self.hub = hub
|
||||||
|
self.asrv = hub.asrv
|
||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
@@ -60,6 +61,8 @@ class Up2k(object):
|
|||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.hashq = Queue()
|
self.hashq = Queue()
|
||||||
self.tagq = Queue()
|
self.tagq = Queue()
|
||||||
|
self.n_hashq = 0
|
||||||
|
self.n_tagq = 0
|
||||||
self.volstate = {}
|
self.volstate = {}
|
||||||
self.registry = {}
|
self.registry = {}
|
||||||
self.entags = {}
|
self.entags = {}
|
||||||
@@ -94,15 +97,17 @@ class Up2k(object):
|
|||||||
self.log("could not initialize sqlite3, will use in-memory registry only")
|
self.log("could not initialize sqlite3, will use in-memory registry only")
|
||||||
|
|
||||||
if self.args.no_fastboot:
|
if self.args.no_fastboot:
|
||||||
self.deferred_init(all_vols)
|
self.deferred_init()
|
||||||
else:
|
else:
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
target=self.deferred_init, args=(all_vols,), name="up2k-deferred-init"
|
target=self.deferred_init,
|
||||||
|
name="up2k-deferred-init",
|
||||||
)
|
)
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def deferred_init(self, all_vols):
|
def deferred_init(self):
|
||||||
|
all_vols = self.asrv.vfs.all_vols
|
||||||
have_e2d = self.init_indexes(all_vols)
|
have_e2d = self.init_indexes(all_vols)
|
||||||
|
|
||||||
if have_e2d:
|
if have_e2d:
|
||||||
@@ -126,8 +131,28 @@ class Up2k(object):
|
|||||||
def log(self, msg, c=0):
|
def log(self, msg, c=0):
|
||||||
self.log_func("up2k", msg + "\033[K", c)
|
self.log_func("up2k", msg + "\033[K", c)
|
||||||
|
|
||||||
def get_volstate(self):
|
def get_state(self):
|
||||||
return json.dumps(self.volstate, indent=4)
|
mtpq = 0
|
||||||
|
q = "select count(w) from mt where k = 't:mtp'"
|
||||||
|
got_lock = self.mutex.acquire(timeout=0.5)
|
||||||
|
if got_lock:
|
||||||
|
for cur in self.cur.values():
|
||||||
|
try:
|
||||||
|
mtpq += cur.execute(q).fetchone()[0]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.mutex.release()
|
||||||
|
else:
|
||||||
|
mtpq = "?"
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"volstate": self.volstate,
|
||||||
|
"scanning": hasattr(self, "pp"),
|
||||||
|
"hashq": self.n_hashq,
|
||||||
|
"tagq": self.n_tagq,
|
||||||
|
"mtpq": mtpq,
|
||||||
|
}
|
||||||
|
return json.dumps(ret, indent=4)
|
||||||
|
|
||||||
def rescan(self, all_vols, scan_vols):
|
def rescan(self, all_vols, scan_vols):
|
||||||
if hasattr(self, "pp"):
|
if hasattr(self, "pp"):
|
||||||
@@ -184,6 +209,8 @@ class Up2k(object):
|
|||||||
self.log(msg, c=3)
|
self.log(msg, c=3)
|
||||||
|
|
||||||
live_vols = []
|
live_vols = []
|
||||||
|
with self.mutex:
|
||||||
|
# only need to protect register_vpath but all in one go feels right
|
||||||
for vol in vols:
|
for vol in vols:
|
||||||
try:
|
try:
|
||||||
os.listdir(vol.realpath)
|
os.listdir(vol.realpath)
|
||||||
@@ -196,7 +223,7 @@ class Up2k(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not self.register_vpath(vol.realpath, vol.flags):
|
if not self.register_vpath(vol.realpath, vol.flags):
|
||||||
# self.log("db not enabled for {}".format(m, vol.realpath))
|
# self.log("db not enable for {}".format(m, vol.realpath))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
live_vols.append(vol)
|
live_vols.append(vol)
|
||||||
@@ -294,7 +321,8 @@ class Up2k(object):
|
|||||||
return have_e2d
|
return have_e2d
|
||||||
|
|
||||||
def register_vpath(self, ptop, flags):
|
def register_vpath(self, ptop, flags):
|
||||||
db_path = os.path.join(ptop, ".hist", "up2k.db")
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
|
db_path = os.path.join(histpath, "up2k.db")
|
||||||
if ptop in self.registry:
|
if ptop in self.registry:
|
||||||
try:
|
try:
|
||||||
return [self.cur[ptop], db_path]
|
return [self.cur[ptop], db_path]
|
||||||
@@ -314,7 +342,7 @@ class Up2k(object):
|
|||||||
self.log(" ".join(sorted(a)) + "\033[0m")
|
self.log(" ".join(sorted(a)) + "\033[0m")
|
||||||
|
|
||||||
reg = {}
|
reg = {}
|
||||||
path = os.path.join(ptop, ".hist", "up2k.snap")
|
path = os.path.join(histpath, "up2k.snap")
|
||||||
if "e2d" in flags and os.path.exists(path):
|
if "e2d" in flags and os.path.exists(path):
|
||||||
with gzip.GzipFile(path, "rb") as f:
|
with gzip.GzipFile(path, "rb") as f:
|
||||||
j = f.read().decode("utf-8")
|
j = f.read().decode("utf-8")
|
||||||
@@ -338,7 +366,7 @@ class Up2k(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.mkdir(os.path.join(ptop, ".hist"))
|
os.makedirs(histpath)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -355,6 +383,7 @@ class Up2k(object):
|
|||||||
def _build_file_index(self, vol, all_vols):
|
def _build_file_index(self, vol, all_vols):
|
||||||
do_vac = False
|
do_vac = False
|
||||||
top = vol.realpath
|
top = vol.realpath
|
||||||
|
nohash = "dhash" in vol.flags
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
cur, _ = self.register_vpath(top, vol.flags)
|
cur, _ = self.register_vpath(top, vol.flags)
|
||||||
|
|
||||||
@@ -369,7 +398,7 @@ class Up2k(object):
|
|||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
excl = [x.replace("/", "\\") for x in excl]
|
excl = [x.replace("/", "\\") for x in excl]
|
||||||
|
|
||||||
n_add = self._build_dir(dbw, top, set(excl), top)
|
n_add = self._build_dir(dbw, top, set(excl), top, nohash)
|
||||||
n_rm = self._drop_lost(dbw[0], top)
|
n_rm = self._drop_lost(dbw[0], top)
|
||||||
if dbw[1]:
|
if dbw[1]:
|
||||||
self.log("commit {} new files".format(dbw[1]))
|
self.log("commit {} new files".format(dbw[1]))
|
||||||
@@ -377,24 +406,25 @@ class Up2k(object):
|
|||||||
|
|
||||||
return True, n_add or n_rm or do_vac
|
return True, n_add or n_rm or do_vac
|
||||||
|
|
||||||
def _build_dir(self, dbw, top, excl, cdir):
|
def _build_dir(self, dbw, top, excl, cdir, nohash):
|
||||||
self.pp.msg = "a{} {}".format(self.pp.n, cdir)
|
self.pp.msg = "a{} {}".format(self.pp.n, cdir)
|
||||||
histdir = os.path.join(top, ".hist")
|
histpath = self.asrv.vfs.histtab[top]
|
||||||
ret = 0
|
ret = 0
|
||||||
g = statdir(self.log, not self.args.no_scandir, False, cdir)
|
g = statdir(self.log, not self.args.no_scandir, False, cdir)
|
||||||
for iname, inf in sorted(g):
|
for iname, inf in sorted(g):
|
||||||
abspath = os.path.join(cdir, iname)
|
abspath = os.path.join(cdir, iname)
|
||||||
lmod = int(inf.st_mtime)
|
lmod = int(inf.st_mtime)
|
||||||
|
sz = inf.st_size
|
||||||
if stat.S_ISDIR(inf.st_mode):
|
if stat.S_ISDIR(inf.st_mode):
|
||||||
if abspath in excl or abspath == histdir:
|
if abspath in excl or abspath == histpath:
|
||||||
continue
|
continue
|
||||||
# self.log(" dir: {}".format(abspath))
|
# self.log(" dir: {}".format(abspath))
|
||||||
ret += self._build_dir(dbw, top, excl, abspath)
|
ret += self._build_dir(dbw, top, excl, abspath, nohash)
|
||||||
else:
|
else:
|
||||||
# self.log("file: {}".format(abspath))
|
# self.log("file: {}".format(abspath))
|
||||||
rp = abspath[len(top) :].replace("\\", "/").strip("/")
|
rp = abspath[len(top) :].replace("\\", "/").strip("/")
|
||||||
rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp]
|
rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp]
|
||||||
sql = "select * from up where rd = ? and fn = ?"
|
sql = "select w, mt, sz from up where rd = ? and fn = ?"
|
||||||
try:
|
try:
|
||||||
c = dbw[0].execute(sql, (rd, fn))
|
c = dbw[0].execute(sql, (rd, fn))
|
||||||
except:
|
except:
|
||||||
@@ -403,18 +433,18 @@ class Up2k(object):
|
|||||||
in_db = list(c.fetchall())
|
in_db = list(c.fetchall())
|
||||||
if in_db:
|
if in_db:
|
||||||
self.pp.n -= 1
|
self.pp.n -= 1
|
||||||
_, dts, dsz, _, _ = in_db[0]
|
dw, dts, dsz = in_db[0]
|
||||||
if len(in_db) > 1:
|
if len(in_db) > 1:
|
||||||
m = "WARN: multiple entries: [{}] => [{}] |{}|\n{}"
|
m = "WARN: multiple entries: [{}] => [{}] |{}|\n{}"
|
||||||
rep_db = "\n".join([repr(x) for x in in_db])
|
rep_db = "\n".join([repr(x) for x in in_db])
|
||||||
self.log(m.format(top, rp, len(in_db), rep_db))
|
self.log(m.format(top, rp, len(in_db), rep_db))
|
||||||
dts = -1
|
dts = -1
|
||||||
|
|
||||||
if dts == lmod and dsz == inf.st_size:
|
if dts == lmod and dsz == sz and (nohash or dw[0] != "#"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
m = "reindex [{}] => [{}] ({}/{}) ({}/{})".format(
|
m = "reindex [{}] => [{}] ({}/{}) ({}/{})".format(
|
||||||
top, rp, dts, lmod, dsz, inf.st_size
|
top, rp, dts, lmod, dsz, sz
|
||||||
)
|
)
|
||||||
self.log(m)
|
self.log(m)
|
||||||
self.db_rm(dbw[0], rd, fn)
|
self.db_rm(dbw[0], rd, fn)
|
||||||
@@ -423,7 +453,11 @@ class Up2k(object):
|
|||||||
in_db = None
|
in_db = None
|
||||||
|
|
||||||
self.pp.msg = "a{} {}".format(self.pp.n, abspath)
|
self.pp.msg = "a{} {}".format(self.pp.n, abspath)
|
||||||
if inf.st_size > 1024 * 1024:
|
|
||||||
|
if nohash:
|
||||||
|
wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)
|
||||||
|
else:
|
||||||
|
if sz > 1024 * 1024:
|
||||||
self.log("file: {}".format(abspath))
|
self.log("file: {}".format(abspath))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -432,8 +466,9 @@ class Up2k(object):
|
|||||||
self.log("hash: {} @ [{}]".format(repr(ex), abspath))
|
self.log("hash: {} @ [{}]".format(repr(ex), abspath))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes)
|
wark = up2k_wark_from_hashlist(self.salt, sz, hashes)
|
||||||
self.db_add(dbw[0], wark, rd, fn, lmod, inf.st_size)
|
|
||||||
|
self.db_add(dbw[0], wark, rd, fn, lmod, sz)
|
||||||
dbw[1] += 1
|
dbw[1] += 1
|
||||||
ret += 1
|
ret += 1
|
||||||
td = time.time() - dbw[2]
|
td = time.time() - dbw[2]
|
||||||
@@ -928,7 +963,7 @@ class Up2k(object):
|
|||||||
def _create_v3(self, cur):
|
def _create_v3(self, cur):
|
||||||
"""
|
"""
|
||||||
collision in 2^(n/2) files where n = bits (6 bits/ch)
|
collision in 2^(n/2) files where n = bits (6 bits/ch)
|
||||||
10*6/2 = 2^30 = 1'073'741'824, 24.1mb idx
|
10*6/2 = 2^30 = 1'073'741'824, 24.1mb idx 1<<(3*10)
|
||||||
12*6/2 = 2^36 = 68'719'476'736, 24.8mb idx
|
12*6/2 = 2^36 = 68'719'476'736, 24.8mb idx
|
||||||
16*6/2 = 2^48 = 281'474'976'710'656, 26.1mb idx
|
16*6/2 = 2^48 = 281'474'976'710'656, 26.1mb idx
|
||||||
"""
|
"""
|
||||||
@@ -976,6 +1011,7 @@ class Up2k(object):
|
|||||||
return self._orz(db_path)
|
return self._orz(db_path)
|
||||||
|
|
||||||
def handle_json(self, cj):
|
def handle_json(self, cj):
|
||||||
|
with self.mutex:
|
||||||
if not self.register_vpath(cj["ptop"], cj["vcfg"]):
|
if not self.register_vpath(cj["ptop"], cj["vcfg"]):
|
||||||
if cj["ptop"] not in self.registry:
|
if cj["ptop"] not in self.registry:
|
||||||
raise Pebkac(410, "location unavailable")
|
raise Pebkac(410, "location unavailable")
|
||||||
@@ -1219,6 +1255,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
if "e2t" in self.flags[ptop]:
|
if "e2t" in self.flags[ptop]:
|
||||||
self.tagq.put([ptop, wark, rd, fn])
|
self.tagq.put([ptop, wark, rd, fn])
|
||||||
|
self.n_tagq += 1
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1344,11 +1381,12 @@ class Up2k(object):
|
|||||||
for k, reg in self.registry.items():
|
for k, reg in self.registry.items():
|
||||||
self._snap_reg(prev, k, reg, discard_interval)
|
self._snap_reg(prev, k, reg, discard_interval)
|
||||||
|
|
||||||
def _snap_reg(self, prev, k, reg, discard_interval):
|
def _snap_reg(self, prev, ptop, reg, discard_interval):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
rm = [x for x in reg.values() if now - x["poke"] > discard_interval]
|
rm = [x for x in reg.values() if now - x["poke"] > discard_interval]
|
||||||
if rm:
|
if rm:
|
||||||
m = "dropping {} abandoned uploads in {}".format(len(rm), k)
|
m = "dropping {} abandoned uploads in {}".format(len(rm), ptop)
|
||||||
vis = [self._vis_job_progress(x) for x in rm]
|
vis = [self._vis_job_progress(x) for x in rm]
|
||||||
self.log("\n".join([m] + vis))
|
self.log("\n".join([m] + vis))
|
||||||
for job in rm:
|
for job in rm:
|
||||||
@@ -1366,21 +1404,21 @@ class Up2k(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
path = os.path.join(k, ".hist", "up2k.snap")
|
path = os.path.join(histpath, "up2k.snap")
|
||||||
if not reg:
|
if not reg:
|
||||||
if k not in prev or prev[k] is not None:
|
if ptop not in prev or prev[ptop] is not None:
|
||||||
prev[k] = None
|
prev[ptop] = None
|
||||||
if os.path.exists(fsenc(path)):
|
if os.path.exists(fsenc(path)):
|
||||||
os.unlink(fsenc(path))
|
os.unlink(fsenc(path))
|
||||||
return
|
return
|
||||||
|
|
||||||
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
||||||
etag = [len(reg), newest]
|
etag = [len(reg), newest]
|
||||||
if etag == prev.get(k, None):
|
if etag == prev.get(ptop, None):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.mkdir(os.path.join(k, ".hist"))
|
os.makedirs(histpath)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1392,14 +1430,21 @@ class Up2k(object):
|
|||||||
atomic_move(path2, path)
|
atomic_move(path2, path)
|
||||||
|
|
||||||
self.log("snap: {} |{}|".format(path, len(reg.keys())))
|
self.log("snap: {} |{}|".format(path, len(reg.keys())))
|
||||||
prev[k] = etag
|
prev[ptop] = etag
|
||||||
|
|
||||||
def _tagger(self):
|
def _tagger(self):
|
||||||
|
with self.mutex:
|
||||||
|
self.n_tagq += 1
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
with self.mutex:
|
||||||
|
self.n_tagq -= 1
|
||||||
|
|
||||||
ptop, wark, rd, fn = self.tagq.get()
|
ptop, wark, rd, fn = self.tagq.get()
|
||||||
if "e2t" not in self.flags[ptop]:
|
if "e2t" not in self.flags[ptop]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# self.log("\n " + repr([ptop, rd, fn]))
|
||||||
abspath = os.path.join(ptop, rd, fn)
|
abspath = os.path.join(ptop, rd, fn)
|
||||||
tags = self.mtag.get(abspath)
|
tags = self.mtag.get(abspath)
|
||||||
ntags1 = len(tags)
|
ntags1 = len(tags)
|
||||||
@@ -1425,8 +1470,16 @@ class Up2k(object):
|
|||||||
self.log("tagged {} ({}+{})".format(abspath, ntags1, len(tags) - ntags1))
|
self.log("tagged {} ({}+{})".format(abspath, ntags1, len(tags) - ntags1))
|
||||||
|
|
||||||
def _hasher(self):
|
def _hasher(self):
|
||||||
|
with self.mutex:
|
||||||
|
self.n_hashq += 1
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
with self.mutex:
|
||||||
|
self.n_hashq -= 1
|
||||||
|
# self.log("hashq {}".format(self.n_hashq))
|
||||||
|
|
||||||
ptop, rd, fn = self.hashq.get()
|
ptop, rd, fn = self.hashq.get()
|
||||||
|
# self.log("hashq {} pop {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
||||||
if "e2d" not in self.flags[ptop]:
|
if "e2d" not in self.flags[ptop]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1439,8 +1492,11 @@ class Up2k(object):
|
|||||||
self.idx_wark(ptop, wark, rd, fn, inf.st_mtime, inf.st_size)
|
self.idx_wark(ptop, wark, rd, fn, inf.st_mtime, inf.st_size)
|
||||||
|
|
||||||
def hash_file(self, ptop, flags, rd, fn):
|
def hash_file(self, ptop, flags, rd, fn):
|
||||||
|
with self.mutex:
|
||||||
self.register_vpath(ptop, flags)
|
self.register_vpath(ptop, flags)
|
||||||
self.hashq.put([ptop, rd, fn])
|
self.hashq.put([ptop, rd, fn])
|
||||||
|
self.n_hashq += 1
|
||||||
|
# self.log("hashq {} push {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
||||||
|
|
||||||
|
|
||||||
def up2k_chunksize(filesize):
|
def up2k_chunksize(filesize):
|
||||||
@@ -1462,9 +1518,12 @@ def up2k_wark_from_hashlist(salt, filesize, hashes):
|
|||||||
ident.extend(hashes)
|
ident.extend(hashes)
|
||||||
ident = "\n".join(ident)
|
ident = "\n".join(ident)
|
||||||
|
|
||||||
hasher = hashlib.sha512()
|
wark = hashlib.sha512(ident.encode("utf-8")).digest()
|
||||||
hasher.update(ident.encode("utf-8"))
|
wark = base64.urlsafe_b64encode(wark)
|
||||||
digest = hasher.digest()[:32]
|
return wark.decode("ascii")[:43]
|
||||||
|
|
||||||
wark = base64.urlsafe_b64encode(digest)
|
|
||||||
return wark.decode("utf-8").rstrip("=")
|
def up2k_wark_from_metadata(salt, sz, lastmod, rd, fn):
|
||||||
|
ret = fsenc("{}\n{}\n{}\n{}\n{}".format(salt, lastmod, sz, rd, fn))
|
||||||
|
ret = base64.urlsafe_b64encode(hashlib.sha512(ret).digest())
|
||||||
|
return "#{}".format(ret[:42].decode("ascii"))
|
||||||
|
|||||||
@@ -254,6 +254,17 @@ def trace(*args, **kwargs):
|
|||||||
nuprint(msg)
|
nuprint(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def min_ex():
|
||||||
|
et, ev, tb = sys.exc_info()
|
||||||
|
tb = traceback.extract_tb(tb, 2)
|
||||||
|
ex = [
|
||||||
|
"{} @ {} <{}>: {}".format(fp.split(os.sep)[-1], ln, fun, txt)
|
||||||
|
for fp, ln, fun, txt in tb
|
||||||
|
]
|
||||||
|
ex.append("{}: {}".format(et.__name__, ev))
|
||||||
|
return "\n".join(ex)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def ren_open(fname, *args, **kwargs):
|
def ren_open(fname, *args, **kwargs):
|
||||||
fdir = kwargs.pop("fdir", None)
|
fdir = kwargs.pop("fdir", None)
|
||||||
@@ -568,8 +579,10 @@ def read_header(sr):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if len(ret) > ofs + 4:
|
||||||
sr.unrecv(ret[ofs + 4 :])
|
sr.unrecv(ret[ofs + 4 :])
|
||||||
return ret[:ofs].decode("utf-8", "surrogateescape").split("\r\n")
|
|
||||||
|
return ret[:ofs].decode("utf-8", "surrogateescape").lstrip("\r\n").split("\r\n")
|
||||||
|
|
||||||
|
|
||||||
def humansize(sz, terse=False):
|
def humansize(sz, terse=False):
|
||||||
@@ -985,8 +998,8 @@ def guess_mime(url, fallback="application/octet-stream"):
|
|||||||
def runcmd(*argv):
|
def runcmd(*argv):
|
||||||
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
stdout = stdout.decode("utf-8")
|
stdout = stdout.decode("utf-8", "replace")
|
||||||
stderr = stderr.decode("utf-8")
|
stderr = stderr.decode("utf-8", "replace")
|
||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
583
copyparty/web/baguettebox.js
Normal file
583
copyparty/web/baguettebox.js
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
/*!
|
||||||
|
* baguetteBox.js
|
||||||
|
* @author feimosi
|
||||||
|
* @version 1.11.1-mod
|
||||||
|
* @url https://github.com/feimosi/baguetteBox.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.baguetteBox = (function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {},
|
||||||
|
defaults = {
|
||||||
|
captions: true,
|
||||||
|
buttons: 'auto',
|
||||||
|
noScrollbars: false,
|
||||||
|
bodyClass: 'baguetteBox-open',
|
||||||
|
titleTag: false,
|
||||||
|
async: false,
|
||||||
|
preload: 2,
|
||||||
|
animation: 'slideIn',
|
||||||
|
afterShow: null,
|
||||||
|
afterHide: null,
|
||||||
|
onChange: null,
|
||||||
|
},
|
||||||
|
overlay, slider, previousButton, nextButton, closeButton,
|
||||||
|
currentGallery = [],
|
||||||
|
currentIndex = 0,
|
||||||
|
isOverlayVisible = false,
|
||||||
|
touch = {}, // start-pos
|
||||||
|
touchFlag = false, // busy
|
||||||
|
regex = /.+\.(gif|jpe?g|png|webp)/i,
|
||||||
|
data = {}, // all galleries
|
||||||
|
imagesElements = [],
|
||||||
|
documentLastFocus = null;
|
||||||
|
|
||||||
|
var overlayClickHandler = function (event) {
|
||||||
|
if (event.target.id.indexOf('baguette-img') !== -1) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var touchstartHandler = function (event) {
|
||||||
|
touch.count++;
|
||||||
|
if (touch.count > 1) {
|
||||||
|
touch.multitouch = true;
|
||||||
|
}
|
||||||
|
touch.startX = event.changedTouches[0].pageX;
|
||||||
|
touch.startY = event.changedTouches[0].pageY;
|
||||||
|
};
|
||||||
|
var touchmoveHandler = function (event) {
|
||||||
|
if (touchFlag || touch.multitouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
||||||
|
var touchEvent = event.touches[0] || event.changedTouches[0];
|
||||||
|
if (touchEvent.pageX - touch.startX > 40) {
|
||||||
|
touchFlag = true;
|
||||||
|
showPreviousImage();
|
||||||
|
} else if (touchEvent.pageX - touch.startX < -40) {
|
||||||
|
touchFlag = true;
|
||||||
|
showNextImage();
|
||||||
|
} else if (touch.startY - touchEvent.pageY > 100) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var touchendHandler = function () {
|
||||||
|
touch.count--;
|
||||||
|
if (touch.count <= 0) {
|
||||||
|
touch.multitouch = false;
|
||||||
|
}
|
||||||
|
touchFlag = false;
|
||||||
|
};
|
||||||
|
var contextmenuHandler = function () {
|
||||||
|
touchendHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
var trapFocusInsideOverlay = function (event) {
|
||||||
|
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
|
||||||
|
event.stopPropagation();
|
||||||
|
initFocus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function run(selector, userOptions) {
|
||||||
|
buildOverlay();
|
||||||
|
removeFromCache(selector);
|
||||||
|
return bindImageClickListeners(selector, userOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindImageClickListeners(selector, userOptions) {
|
||||||
|
var galleryNodeList = document.querySelectorAll(selector);
|
||||||
|
var selectorData = {
|
||||||
|
galleries: [],
|
||||||
|
nodeList: galleryNodeList
|
||||||
|
};
|
||||||
|
data[selector] = selectorData;
|
||||||
|
|
||||||
|
[].forEach.call(galleryNodeList, function (galleryElement) {
|
||||||
|
if (userOptions && userOptions.filter) {
|
||||||
|
regex = userOptions.filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagsNodeList = [];
|
||||||
|
if (galleryElement.tagName === 'A') {
|
||||||
|
tagsNodeList = [galleryElement];
|
||||||
|
} else {
|
||||||
|
tagsNodeList = galleryElement.getElementsByTagName('a');
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
||||||
|
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
|
||||||
|
return regex.test(element.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (tagsNodeList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gallery = [];
|
||||||
|
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
||||||
|
var imageElementClickHandler = function (event) {
|
||||||
|
if (event && event.ctrlKey)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
||||||
|
prepareOverlay(gallery, userOptions);
|
||||||
|
showOverlay(imageIndex);
|
||||||
|
};
|
||||||
|
var imageItem = {
|
||||||
|
eventHandler: imageElementClickHandler,
|
||||||
|
imageElement: imageElement
|
||||||
|
};
|
||||||
|
bind(imageElement, 'click', imageElementClickHandler);
|
||||||
|
gallery.push(imageItem);
|
||||||
|
});
|
||||||
|
selectorData.galleries.push(gallery);
|
||||||
|
});
|
||||||
|
|
||||||
|
return selectorData.galleries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCachedData() {
|
||||||
|
for (var selector in data) {
|
||||||
|
if (data.hasOwnProperty(selector)) {
|
||||||
|
removeFromCache(selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromCache(selector) {
|
||||||
|
if (!data.hasOwnProperty(selector)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var galleries = data[selector].galleries;
|
||||||
|
[].forEach.call(galleries, function (gallery) {
|
||||||
|
[].forEach.call(gallery, function (imageItem) {
|
||||||
|
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentGallery === gallery) {
|
||||||
|
currentGallery = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delete data[selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOverlay() {
|
||||||
|
overlay = ebi('baguetteBox-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
slider = ebi('baguetteBox-slider');
|
||||||
|
previousButton = ebi('previous-button');
|
||||||
|
nextButton = ebi('next-button');
|
||||||
|
closeButton = ebi('close-button');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
overlay = mknod('div');
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.id = 'baguetteBox-overlay';
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(overlay);
|
||||||
|
|
||||||
|
slider = mknod('div');
|
||||||
|
slider.id = 'baguetteBox-slider';
|
||||||
|
overlay.appendChild(slider);
|
||||||
|
|
||||||
|
previousButton = mknod('button');
|
||||||
|
previousButton.setAttribute('type', 'button');
|
||||||
|
previousButton.id = 'previous-button';
|
||||||
|
previousButton.setAttribute('aria-label', 'Previous');
|
||||||
|
previousButton.innerHTML = '<';
|
||||||
|
overlay.appendChild(previousButton);
|
||||||
|
|
||||||
|
nextButton = mknod('button');
|
||||||
|
nextButton.setAttribute('type', 'button');
|
||||||
|
nextButton.id = 'next-button';
|
||||||
|
nextButton.setAttribute('aria-label', 'Next');
|
||||||
|
nextButton.innerHTML = '>';
|
||||||
|
overlay.appendChild(nextButton);
|
||||||
|
|
||||||
|
closeButton = mknod('button');
|
||||||
|
closeButton.setAttribute('type', 'button');
|
||||||
|
closeButton.id = 'close-button';
|
||||||
|
closeButton.setAttribute('aria-label', 'Close');
|
||||||
|
closeButton.innerHTML = '×';
|
||||||
|
overlay.appendChild(closeButton);
|
||||||
|
|
||||||
|
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
|
||||||
|
|
||||||
|
bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyDownHandler(event) {
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 37: // Left
|
||||||
|
showPreviousImage();
|
||||||
|
break;
|
||||||
|
case 39: // Right
|
||||||
|
showNextImage();
|
||||||
|
break;
|
||||||
|
case 27: // Esc
|
||||||
|
hideOverlay();
|
||||||
|
break;
|
||||||
|
case 36: // Home
|
||||||
|
showFirstImage(event);
|
||||||
|
break;
|
||||||
|
case 35: // End
|
||||||
|
showLastImage(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var passiveSupp = false;
|
||||||
|
try {
|
||||||
|
var opts = {
|
||||||
|
get passive() {
|
||||||
|
passiveSupp = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('test', null, opts);
|
||||||
|
window.removeEventListener('test', null, opts);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
passiveSupp = false;
|
||||||
|
}
|
||||||
|
var passiveEvent = passiveSupp ? { passive: false } : null;
|
||||||
|
var nonPassiveEvent = passiveSupp ? { passive: true } : null;
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
bind(overlay, 'click', overlayClickHandler);
|
||||||
|
bind(previousButton, 'click', showPreviousImage);
|
||||||
|
bind(nextButton, 'click', showNextImage);
|
||||||
|
bind(closeButton, 'click', hideOverlay);
|
||||||
|
bind(slider, 'contextmenu', contextmenuHandler);
|
||||||
|
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
|
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
|
bind(overlay, 'touchend', touchendHandler);
|
||||||
|
bind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindEvents() {
|
||||||
|
unbind(overlay, 'click', overlayClickHandler);
|
||||||
|
unbind(previousButton, 'click', showPreviousImage);
|
||||||
|
unbind(nextButton, 'click', showNextImage);
|
||||||
|
unbind(closeButton, 'click', hideOverlay);
|
||||||
|
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||||
|
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
|
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
|
unbind(overlay, 'touchend', touchendHandler);
|
||||||
|
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareOverlay(gallery, userOptions) {
|
||||||
|
if (currentGallery === gallery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentGallery = gallery;
|
||||||
|
setOptions(userOptions);
|
||||||
|
slider.innerHTML = '';
|
||||||
|
imagesElements.length = 0;
|
||||||
|
|
||||||
|
var imagesFiguresIds = [];
|
||||||
|
var imagesCaptionsIds = [];
|
||||||
|
for (var i = 0, fullImage; i < gallery.length; i++) {
|
||||||
|
fullImage = mknod('div');
|
||||||
|
fullImage.className = 'full-image';
|
||||||
|
fullImage.id = 'baguette-img-' + i;
|
||||||
|
imagesElements.push(fullImage);
|
||||||
|
|
||||||
|
imagesFiguresIds.push('baguetteBox-figure-' + i);
|
||||||
|
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
|
||||||
|
slider.appendChild(imagesElements[i]);
|
||||||
|
}
|
||||||
|
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
||||||
|
overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptions(newOptions) {
|
||||||
|
if (!newOptions) {
|
||||||
|
newOptions = {};
|
||||||
|
}
|
||||||
|
for (var item in defaults) {
|
||||||
|
options[item] = defaults[item];
|
||||||
|
if (typeof newOptions[item] !== 'undefined') {
|
||||||
|
options[item] = newOptions[item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
||||||
|
options.animation === 'slideIn' ? '' : 'none');
|
||||||
|
|
||||||
|
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
|
||||||
|
options.buttons = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOverlay(chosenImageIndex) {
|
||||||
|
if (options.noScrollbars) {
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.body.style.overflowY = 'scroll';
|
||||||
|
}
|
||||||
|
if (overlay.style.display === 'block') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(document, 'keydown', keyDownHandler);
|
||||||
|
currentIndex = chosenImageIndex;
|
||||||
|
touch = {
|
||||||
|
count: 0,
|
||||||
|
startX: null,
|
||||||
|
startY: null
|
||||||
|
};
|
||||||
|
loadImage(currentIndex, function () {
|
||||||
|
preloadNext(currentIndex);
|
||||||
|
preloadPrev(currentIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateOffset();
|
||||||
|
overlay.style.display = 'block';
|
||||||
|
// Fade in overlay
|
||||||
|
setTimeout(function () {
|
||||||
|
overlay.className = 'visible';
|
||||||
|
if (options.bodyClass && document.body.classList) {
|
||||||
|
document.body.classList.add(options.bodyClass);
|
||||||
|
}
|
||||||
|
if (options.afterShow) {
|
||||||
|
options.afterShow();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
if (options.onChange) {
|
||||||
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
}
|
||||||
|
documentLastFocus = document.activeElement;
|
||||||
|
initFocus();
|
||||||
|
isOverlayVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFocus() {
|
||||||
|
if (options.buttons) {
|
||||||
|
previousButton.focus();
|
||||||
|
} else {
|
||||||
|
closeButton.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOverlay(e) {
|
||||||
|
ev(e);
|
||||||
|
if (options.noScrollbars) {
|
||||||
|
document.documentElement.style.overflowY = 'auto';
|
||||||
|
document.body.style.overflowY = 'auto';
|
||||||
|
}
|
||||||
|
if (overlay.style.display === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
// Fade out and hide the overlay
|
||||||
|
overlay.className = '';
|
||||||
|
setTimeout(function () {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
if (options.bodyClass && document.body.classList) {
|
||||||
|
document.body.classList.remove(options.bodyClass);
|
||||||
|
}
|
||||||
|
if (options.afterHide) {
|
||||||
|
options.afterHide();
|
||||||
|
}
|
||||||
|
documentLastFocus && documentLastFocus.focus();
|
||||||
|
isOverlayVisible = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(index, callback) {
|
||||||
|
var imageContainer = imagesElements[index];
|
||||||
|
var galleryItem = currentGallery[index];
|
||||||
|
|
||||||
|
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
|
||||||
|
return; // out-of-bounds or gallery dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageContainer.getElementsByTagName('img')[0]) {
|
||||||
|
// image is loaded, cb and bail
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageElement = galleryItem.imageElement,
|
||||||
|
imageSrc = imageElement.href,
|
||||||
|
thumbnailElement = imageElement.getElementsByTagName('img')[0],
|
||||||
|
imageCaption = typeof options.captions === 'function' ?
|
||||||
|
options.captions.call(currentGallery, imageElement) :
|
||||||
|
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||||
|
|
||||||
|
var figure = mknod('figure');
|
||||||
|
figure.id = 'baguetteBox-figure-' + index;
|
||||||
|
figure.innerHTML = '<div class="baguetteBox-spinner">' +
|
||||||
|
'<div class="baguetteBox-double-bounce1"></div>' +
|
||||||
|
'<div class="baguetteBox-double-bounce2"></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
if (options.captions && imageCaption) {
|
||||||
|
var figcaption = mknod('figcaption');
|
||||||
|
figcaption.id = 'baguetteBox-figcaption-' + index;
|
||||||
|
figcaption.innerHTML = imageCaption;
|
||||||
|
figure.appendChild(figcaption);
|
||||||
|
}
|
||||||
|
imageContainer.appendChild(figure);
|
||||||
|
|
||||||
|
var image = mknod('img');
|
||||||
|
image.onload = function () {
|
||||||
|
// Remove loader element
|
||||||
|
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
|
||||||
|
figure.removeChild(spinner);
|
||||||
|
if (!options.async && callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
image.setAttribute('src', imageSrc);
|
||||||
|
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||||
|
if (options.titleTag && imageCaption) {
|
||||||
|
image.title = imageCaption;
|
||||||
|
}
|
||||||
|
figure.appendChild(image);
|
||||||
|
|
||||||
|
if (options.async && callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNextImage(e) {
|
||||||
|
ev(e);
|
||||||
|
return show(currentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreviousImage(e) {
|
||||||
|
ev(e);
|
||||||
|
return show(currentIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFirstImage(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
return show(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLastImage(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
return show(currentGallery.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the gallery to a specific index
|
||||||
|
* @param `index` {number} - the position of the image
|
||||||
|
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
|
||||||
|
* @return {boolean} - true on success or false if the index is invalid
|
||||||
|
*/
|
||||||
|
function show(index, gallery) {
|
||||||
|
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
||||||
|
prepareOverlay(gallery, options);
|
||||||
|
showOverlay(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
if (options.animation) {
|
||||||
|
bounceAnimation('left');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (index >= imagesElements.length) {
|
||||||
|
if (options.animation) {
|
||||||
|
bounceAnimation('right');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = index;
|
||||||
|
loadImage(currentIndex, function () {
|
||||||
|
preloadNext(currentIndex);
|
||||||
|
preloadPrev(currentIndex);
|
||||||
|
});
|
||||||
|
updateOffset();
|
||||||
|
|
||||||
|
if (options.onChange) {
|
||||||
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the bounce animation
|
||||||
|
* @param {('left'|'right')} direction - Direction of the movement
|
||||||
|
*/
|
||||||
|
function bounceAnimation(direction) {
|
||||||
|
slider.className = 'bounce-from-' + direction;
|
||||||
|
setTimeout(function () {
|
||||||
|
slider.className = '';
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOffset() {
|
||||||
|
var offset = -currentIndex * 100 + '%';
|
||||||
|
if (options.animation === 'fadeIn') {
|
||||||
|
slider.style.opacity = 0;
|
||||||
|
setTimeout(function () {
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
||||||
|
slider.style.opacity = 1;
|
||||||
|
}, 400);
|
||||||
|
} else {
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadNext(index) {
|
||||||
|
if (index - currentIndex >= options.preload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadImage(index + 1, function () {
|
||||||
|
preloadNext(index + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadPrev(index) {
|
||||||
|
if (currentIndex - index >= options.preload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadImage(index - 1, function () {
|
||||||
|
preloadPrev(index - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind(element, event, callback, options) {
|
||||||
|
element.addEventListener(event, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbind(element, event, callback, options) {
|
||||||
|
element.removeEventListener(event, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlugin() {
|
||||||
|
unbindEvents();
|
||||||
|
clearCachedData();
|
||||||
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
document.getElementsByTagName('body')[0].removeChild(ebi('baguetteBox-overlay'));
|
||||||
|
data = {};
|
||||||
|
currentGallery = [];
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: run,
|
||||||
|
show: show,
|
||||||
|
showNext: showNextImage,
|
||||||
|
showPrevious: showPreviousImage,
|
||||||
|
hide: hideOverlay,
|
||||||
|
destroy: destroyPlugin
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -644,7 +644,6 @@ input[type="checkbox"]:checked+label {
|
|||||||
}
|
}
|
||||||
#treeul a+a {
|
#treeul a+a {
|
||||||
width: calc(100% - 2em);
|
width: calc(100% - 2em);
|
||||||
background: #333;
|
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
#treeul a+a:hover {
|
#treeul a+a:hover {
|
||||||
@@ -751,9 +750,12 @@ input[type="checkbox"]:checked+label {
|
|||||||
font-family: monospace, monospace;
|
font-family: monospace, monospace;
|
||||||
line-height: 2em;
|
line-height: 2em;
|
||||||
}
|
}
|
||||||
#griden.on+#thumbs {
|
#thumbs {
|
||||||
opacity: .3;
|
opacity: .3;
|
||||||
}
|
}
|
||||||
|
#griden.on+#thumbs {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
#ghead {
|
#ghead {
|
||||||
background: #3c3c3c;
|
background: #3c3c3c;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
@@ -798,6 +800,13 @@ html.light #ghead {
|
|||||||
padding: .2em .3em;
|
padding: .2em .3em;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
#ggrid span.dir:before {
|
||||||
|
content: '📂';
|
||||||
|
line-height: 0;
|
||||||
|
font-size: 2em;
|
||||||
|
display: inline-block;
|
||||||
|
margin: -.7em .1em -.5em -.3em;
|
||||||
|
}
|
||||||
#ggrid a:hover {
|
#ggrid a:hover {
|
||||||
background: #444;
|
background: #444;
|
||||||
border-color: #555;
|
border-color: #555;
|
||||||
@@ -910,6 +919,7 @@ html.light #files {
|
|||||||
}
|
}
|
||||||
html.light #files thead th {
|
html.light #files thead th {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
html.light #files tr td {
|
html.light #files tr td {
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
@@ -1022,3 +1032,160 @@ html.light #tree::-webkit-scrollbar {
|
|||||||
#tree::-webkit-scrollbar-thumb {
|
#tree::-webkit-scrollbar-thumb {
|
||||||
background: #da0;
|
background: #da0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#baguetteBox-overlay {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000000;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
transition: opacity .3s ease;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image figure {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image img {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image figcaption {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: normal;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay figcaption a {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: .4em;
|
||||||
|
padding: .3em .6em;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
height: 50%;
|
||||||
|
width: 1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: left .2s ease, transform .2s ease;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider.bounce-from-right {
|
||||||
|
animation: bounceFromRight .4s ease-out;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider.bounce-from-left {
|
||||||
|
animation: bounceFromLeft .4s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes bounceFromRight {
|
||||||
|
0% {margin-left: 0}
|
||||||
|
50% {margin-left: -30px}
|
||||||
|
100% {margin-left: 0}
|
||||||
|
}
|
||||||
|
@keyframes bounceFromLeft {
|
||||||
|
0% {margin-left: 0}
|
||||||
|
50% {margin-left: 30px}
|
||||||
|
100% {margin-left: 0}
|
||||||
|
}
|
||||||
|
.baguetteBox-button#next-button,
|
||||||
|
.baguetteBox-button#previous-button {
|
||||||
|
top: 50%;
|
||||||
|
top: calc(50% - 30px);
|
||||||
|
width: 44px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.baguetteBox-button {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 15%;
|
||||||
|
background: rgba(50, 50, 50, 0.5);
|
||||||
|
color: #ddd;
|
||||||
|
font: 1.6em sans-serif;
|
||||||
|
transition: background-color .3s ease;
|
||||||
|
}
|
||||||
|
.baguetteBox-button:focus,
|
||||||
|
.baguetteBox-button:hover {
|
||||||
|
background: rgba(50, 50, 50, 0.9);
|
||||||
|
}
|
||||||
|
#next-button {
|
||||||
|
right: 2%;
|
||||||
|
}
|
||||||
|
#previous-button {
|
||||||
|
left: 2%;
|
||||||
|
}
|
||||||
|
#close-button {
|
||||||
|
top: 20px;
|
||||||
|
right: 2%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
.baguetteBox-button svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.baguetteBox-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-left: -20px;
|
||||||
|
}
|
||||||
|
.baguetteBox-double-bounce1,
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
opacity: .6;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: bounce 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {transform: scale(0)}
|
||||||
|
50% {transform: scale(1)}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css{{ ts }}">
|
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css{{ ts }}">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}">
|
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}">
|
||||||
|
{%- if css %}
|
||||||
|
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}{{ ts }}">
|
||||||
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ var have_webp = null;
|
|||||||
img.onerror = function () {
|
img.onerror = function () {
|
||||||
have_webp = false;
|
have_webp = false;
|
||||||
};
|
};
|
||||||
img.src = "data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA";
|
img.src = "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@ function MPlayer() {
|
|||||||
this.au = null;
|
this.au = null;
|
||||||
this.au_native = null;
|
this.au_native = null;
|
||||||
this.au_ogvjs = null;
|
this.au_ogvjs = null;
|
||||||
this.cover_url = '';
|
|
||||||
this.tracks = {};
|
this.tracks = {};
|
||||||
this.order = [];
|
this.order = [];
|
||||||
|
|
||||||
@@ -710,8 +709,9 @@ function autoplay_blocked(seek) {
|
|||||||
|
|
||||||
|
|
||||||
var thegrid = (function () {
|
var thegrid = (function () {
|
||||||
var lfiles = ebi('files');
|
var lfiles = ebi('files'),
|
||||||
var gfiles = document.createElement('div');
|
gfiles = document.createElement('div');
|
||||||
|
|
||||||
gfiles.setAttribute('id', 'gfiles');
|
gfiles.setAttribute('id', 'gfiles');
|
||||||
gfiles.style.display = 'none';
|
gfiles.style.display = 'none';
|
||||||
gfiles.innerHTML = (
|
gfiles.innerHTML = (
|
||||||
@@ -733,7 +733,8 @@ var thegrid = (function () {
|
|||||||
'en': bcfg_get('griden', false),
|
'en': bcfg_get('griden', false),
|
||||||
'sel': bcfg_get('gridsel', false),
|
'sel': bcfg_get('gridsel', false),
|
||||||
'sz': fcfg_get('gridsz', 10),
|
'sz': fcfg_get('gridsz', 10),
|
||||||
'isdirty': true
|
'isdirty': true,
|
||||||
|
'bbox': null
|
||||||
};
|
};
|
||||||
|
|
||||||
ebi('thumbs').onclick = function (e) {
|
ebi('thumbs').onclick = function (e) {
|
||||||
@@ -858,14 +859,15 @@ var thegrid = (function () {
|
|||||||
href = esc(ao.getAttribute('href')),
|
href = esc(ao.getAttribute('href')),
|
||||||
ref = ao.getAttribute('id'),
|
ref = ao.getAttribute('id'),
|
||||||
isdir = href.split('?')[0].slice(-1)[0] == '/',
|
isdir = href.split('?')[0].slice(-1)[0] == '/',
|
||||||
|
ac = isdir ? ' class="dir"' : '',
|
||||||
ihref = href;
|
ihref = href;
|
||||||
|
|
||||||
if (isdir) {
|
if (r.thumbs) {
|
||||||
ihref = '/.cpr/ico/folder'
|
|
||||||
}
|
|
||||||
else if (r.thumbs) {
|
|
||||||
ihref += (ihref.indexOf('?') === -1 ? '?' : '&') + 'th=' + (have_webp ? 'w' : 'j');
|
ihref += (ihref.indexOf('?') === -1 ? '?' : '&') + 'th=' + (have_webp ? 'w' : 'j');
|
||||||
}
|
}
|
||||||
|
else if (isdir) {
|
||||||
|
ihref = '/.cpr/ico/folder';
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
var ar = href.split('?')[0].split('.');
|
var ar = href.split('?')[0].split('.');
|
||||||
if (ar.length > 1)
|
if (ar.length > 1)
|
||||||
@@ -886,14 +888,42 @@ var thegrid = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html.push('<a href="' + href + '" ref="' + ref + '"><img src="' +
|
html.push('<a href="' + href + '" ref="' + ref + '"><img src="' +
|
||||||
ihref + '" /><span>' + ao.innerHTML + '</span></a>');
|
ihref + '" /><span' + ac + '>' + ao.innerHTML + '</span></a>');
|
||||||
}
|
}
|
||||||
lfiles.style.display = 'none';
|
lfiles.style.display = 'none';
|
||||||
gfiles.style.display = 'block';
|
gfiles.style.display = 'block';
|
||||||
ebi('ggrid').innerHTML = html.join('\n');
|
ebi('ggrid').innerHTML = html.join('\n');
|
||||||
|
r.bagit();
|
||||||
r.loadsel();
|
r.loadsel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.bagit = function () {
|
||||||
|
if (!window.baguetteBox)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (r.bbox)
|
||||||
|
baguetteBox.destroy();
|
||||||
|
|
||||||
|
r.bbox = baguetteBox.run('#ggrid', {
|
||||||
|
captions: function (g) {
|
||||||
|
var idx = -1,
|
||||||
|
h = '' + g;
|
||||||
|
|
||||||
|
for (var a = 0; a < r.bbox.length; a++)
|
||||||
|
if (r.bbox[a].imageElement == g)
|
||||||
|
idx = a;
|
||||||
|
|
||||||
|
return '<a download href="' + h +
|
||||||
|
'">' + (idx + 1) + ' / ' + r.bbox.length + ' -- ' +
|
||||||
|
esc(uricom_dec(h.split('/').slice(-1)[0])[0]) + '</a>';
|
||||||
|
}
|
||||||
|
})[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
import_js('/.cpr/baguettebox.js', r.bagit);
|
||||||
|
}, 1);
|
||||||
|
|
||||||
if (r.en) {
|
if (r.en) {
|
||||||
loadgrid();
|
loadgrid();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,23 @@ a {
|
|||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
padding: .2em .8em;
|
padding: .2em .8em;
|
||||||
}
|
}
|
||||||
td, th {
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.vols td,
|
||||||
|
.vols th {
|
||||||
padding: .3em .6em;
|
padding: .3em .6em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.num {
|
||||||
|
border-right: 1px solid #bbb;
|
||||||
|
}
|
||||||
|
.num td {
|
||||||
|
padding: .1em .7em .1em 0;
|
||||||
|
}
|
||||||
|
.num td:first-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
.btns {
|
.btns {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
@@ -58,3 +71,6 @@ html.dark input {
|
|||||||
padding: .5em .7em;
|
padding: .5em .7em;
|
||||||
margin: 0 .5em 0 0;
|
margin: 0 .5em 0 0;
|
||||||
}
|
}
|
||||||
|
html.dark .num {
|
||||||
|
border-color: #777;
|
||||||
|
}
|
||||||
@@ -15,7 +15,15 @@
|
|||||||
|
|
||||||
{%- if avol %}
|
{%- if avol %}
|
||||||
<h1>admin panel:</h1>
|
<h1>admin panel:</h1>
|
||||||
<table>
|
<table><tr><td> <!-- hehehe -->
|
||||||
|
<table class="num">
|
||||||
|
<tr><td>scanning</td><td>{{ scanning }}</td></tr>
|
||||||
|
<tr><td>hash-q</td><td>{{ hashq }}</td></tr>
|
||||||
|
<tr><td>tag-q</td><td>{{ tagq }}</td></tr>
|
||||||
|
<tr><td>mtp-q</td><td>{{ mtpq }}</td></tr>
|
||||||
|
</table>
|
||||||
|
</td><td>
|
||||||
|
<table class="vols">
|
||||||
<thead><tr><th>vol</th><th>action</th><th>status</th></tr></thead>
|
<thead><tr><th>vol</th><th>action</th><th>status</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for mp in avol %}
|
{% for mp in avol %}
|
||||||
@@ -25,6 +33,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</td></tr></table>
|
||||||
<div class="btns">
|
<div class="btns">
|
||||||
<a href="{{ avol[0] }}?stack">dump stack</a>
|
<a href="{{ avol[0] }}?stack">dump stack</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
docs/README.md
Normal file
11
docs/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## [`minimal-up2k.html`](minimal-up2k.html)
|
||||||
|
* save as `.epilogue.html` inside a folder to [simplify the ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||||
|
|
||||||
|
## [`browser.css`](browser.css)
|
||||||
|
* example for `--css-browser`
|
||||||
|
|
||||||
|
## [`rclone.md`](rclone.md)
|
||||||
|
* notes on using rclone as a fuse client/server
|
||||||
|
|
||||||
|
## [`example.conf`](example.conf)
|
||||||
|
* example config file for `-c` which never really happened
|
||||||
29
docs/browser.css
Normal file
29
docs/browser.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
html {
|
||||||
|
background: url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed;
|
||||||
|
}
|
||||||
|
#files th {
|
||||||
|
background: rgba(32, 32, 32, 0.9) !important;
|
||||||
|
}
|
||||||
|
#ops,
|
||||||
|
#treeul,
|
||||||
|
#files td {
|
||||||
|
background: rgba(32, 32, 32, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html.light {
|
||||||
|
background: url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed;
|
||||||
|
}
|
||||||
|
html.light #files th {
|
||||||
|
background: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
html.light #ops,
|
||||||
|
html.light #treeul,
|
||||||
|
html.light #files td {
|
||||||
|
background: rgba(248, 248, 248, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#files * {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
@@ -208,7 +208,7 @@ find | grep -E '\.css$' | while IFS= read -r f; do
|
|||||||
}
|
}
|
||||||
!/\}$/ {printf "%s",$0;next}
|
!/\}$/ {printf "%s",$0;next}
|
||||||
1
|
1
|
||||||
' <$f | gsed 's/;\}$/}/' >t
|
' <$f | sed 's/;\}$/}/' >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
done
|
done
|
||||||
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
||||||
@@ -223,7 +223,7 @@ command -v pigz &&
|
|||||||
pk='gzip'
|
pk='gzip'
|
||||||
|
|
||||||
echo "$pk"
|
echo "$pk"
|
||||||
find | grep -E '\.(js|css)$' | while IFS= read -r f; do
|
find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do
|
||||||
echo -n .
|
echo -n .
|
||||||
$pk "$f"
|
$pk "$f"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ set -ex
|
|||||||
|
|
||||||
pids=()
|
pids=()
|
||||||
for py in python{2,3}; do
|
for py in python{2,3}; do
|
||||||
$py -m unittest discover -s tests >/dev/null &
|
nice $py -m unittest discover -s tests >/dev/null &
|
||||||
pids+=($!)
|
pids+=($!)
|
||||||
done
|
done
|
||||||
|
|
||||||
|
python3 scripts/test/smoketest.py &
|
||||||
|
pids+=($!)
|
||||||
|
|
||||||
for pid in ${pids[@]}; do
|
for pid in ${pids[@]}; do
|
||||||
wait $pid
|
wait $pid
|
||||||
done
|
done
|
||||||
|
|||||||
209
scripts/test/smoketest.py
Normal file
209
scripts/test/smoketest.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import tempfile
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
|
||||||
|
CPP = []
|
||||||
|
|
||||||
|
|
||||||
|
class Cpp(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
args = [sys.executable, "-m", "copyparty"] + args
|
||||||
|
print(" ".join([shlex.quote(x) for x in args]))
|
||||||
|
|
||||||
|
self.ls_pre = set(list(os.listdir()))
|
||||||
|
self.p = sp.Popen(args)
|
||||||
|
# , stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
|
|
||||||
|
self.t = threading.Thread(target=self._run)
|
||||||
|
self.t.daemon = True
|
||||||
|
self.t.start()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
self.so, self.se = self.p.communicate()
|
||||||
|
|
||||||
|
def stop(self, wait):
|
||||||
|
if wait:
|
||||||
|
os.kill(self.p.pid, signal.SIGINT)
|
||||||
|
self.t.join(timeout=2)
|
||||||
|
else:
|
||||||
|
self.p.kill() # macos py3.8
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
t = os.listdir()
|
||||||
|
for f in t:
|
||||||
|
if f not in self.ls_pre and f.startswith("up."):
|
||||||
|
os.unlink(f)
|
||||||
|
|
||||||
|
def await_idle(self, ub, timeout):
|
||||||
|
req = ["scanning</td><td>False", "hash-q</td><td>0", "tag-q</td><td>0"]
|
||||||
|
lim = int(timeout * 10)
|
||||||
|
u = ub + "?h"
|
||||||
|
for n in range(lim):
|
||||||
|
try:
|
||||||
|
time.sleep(0.1)
|
||||||
|
r = requests.get(u, timeout=0.1)
|
||||||
|
for x in req:
|
||||||
|
if x not in r.text:
|
||||||
|
print("ST: {}/{} miss {}".format(n, lim, x))
|
||||||
|
raise Exception()
|
||||||
|
print("ST: idle")
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def tc1():
|
||||||
|
ub = "http://127.0.0.1:4321/"
|
||||||
|
td = os.path.join("srv", "smoketest")
|
||||||
|
try:
|
||||||
|
shutil.rmtree(td)
|
||||||
|
except:
|
||||||
|
if os.path.exists(td):
|
||||||
|
raise
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
try:
|
||||||
|
os.mkdir(td)
|
||||||
|
except:
|
||||||
|
time.sleep(0.1) # win10
|
||||||
|
|
||||||
|
assert os.path.exists(td)
|
||||||
|
|
||||||
|
vidp = os.path.join(tempfile.gettempdir(), "smoketest.h264")
|
||||||
|
if not os.path.exists(vidp):
|
||||||
|
cmd = "ffmpeg -f lavfi -i testsrc=48x32:3 -t 1 -c:v libx264 -tune animation -preset veryslow -crf 69"
|
||||||
|
sp.check_call(cmd.split(" ") + [vidp])
|
||||||
|
|
||||||
|
with open(vidp, "rb") as f:
|
||||||
|
ovid = f.read()
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"-p4321",
|
||||||
|
"-e2dsa",
|
||||||
|
"-e2tsr",
|
||||||
|
"--no-mutagen",
|
||||||
|
"--th-ff-jpg",
|
||||||
|
"--hist",
|
||||||
|
os.path.join(td, "dbm"),
|
||||||
|
]
|
||||||
|
pdirs = []
|
||||||
|
hpaths = {}
|
||||||
|
|
||||||
|
for d1 in ["r", "w", "a"]:
|
||||||
|
pdirs.append("{}/{}".format(td, d1))
|
||||||
|
pdirs.append("{}/{}/j".format(td, d1))
|
||||||
|
for d2 in ["r", "w", "a"]:
|
||||||
|
d = os.path.join(td, d1, "j", d2)
|
||||||
|
pdirs.append(d)
|
||||||
|
os.makedirs(d)
|
||||||
|
|
||||||
|
pdirs = [x.replace("\\", "/") for x in pdirs]
|
||||||
|
udirs = [x.split("/", 2)[2] for x in pdirs]
|
||||||
|
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
||||||
|
for pd, ud, p in zip(pdirs, udirs, perms):
|
||||||
|
if ud[-1] == "j":
|
||||||
|
continue
|
||||||
|
|
||||||
|
hp = None
|
||||||
|
if pd.endswith("st/a"):
|
||||||
|
hp = hpaths[ud] = os.path.join(td, "db1")
|
||||||
|
elif pd[:-1].endswith("a/j/"):
|
||||||
|
hpaths[ud] = os.path.join(td, "dbm")
|
||||||
|
hp = None
|
||||||
|
else:
|
||||||
|
hp = "-"
|
||||||
|
hpaths[ud] = os.path.join(pd, ".hist")
|
||||||
|
|
||||||
|
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
||||||
|
if hp:
|
||||||
|
arg += ":chist=" + hp
|
||||||
|
|
||||||
|
args += ["-v", arg]
|
||||||
|
|
||||||
|
# return
|
||||||
|
cpp = Cpp(args)
|
||||||
|
CPP.append(cpp)
|
||||||
|
cpp.await_idle(ub, 3)
|
||||||
|
|
||||||
|
for d in udirs:
|
||||||
|
vid = ovid + "\n{}".format(d).encode("utf-8")
|
||||||
|
try:
|
||||||
|
requests.post(ub + d, data={"act": "bput"}, files={"f": ("a.h264", vid)})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cpp.clean()
|
||||||
|
|
||||||
|
# GET permission
|
||||||
|
for d, p in zip(udirs, perms):
|
||||||
|
u = "{}{}/a.h264".format(ub, d)
|
||||||
|
r = requests.get(u)
|
||||||
|
ok = bool(r)
|
||||||
|
if ok != (p in ["a"]):
|
||||||
|
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
# stat filesystem
|
||||||
|
for d, p in zip(pdirs, perms):
|
||||||
|
u = "{}/a.h264".format(d)
|
||||||
|
ok = os.path.exists(u)
|
||||||
|
if ok != (p in ["a", "w"]):
|
||||||
|
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
# GET thumbnail, vreify contents
|
||||||
|
for d, p in zip(udirs, perms):
|
||||||
|
u = "{}{}/a.h264?th=j".format(ub, d)
|
||||||
|
r = requests.get(u)
|
||||||
|
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
||||||
|
if ok != (p in ["a"]):
|
||||||
|
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
# check tags
|
||||||
|
cpp.await_idle(ub, 5)
|
||||||
|
for d, p in zip(udirs, perms):
|
||||||
|
u = "{}{}?ls".format(ub, d)
|
||||||
|
r = requests.get(u)
|
||||||
|
j = r.json() if r else False
|
||||||
|
tag = None
|
||||||
|
if j:
|
||||||
|
for f in j["files"]:
|
||||||
|
tag = tag or f["tags"].get("res")
|
||||||
|
|
||||||
|
r_ok = bool(j)
|
||||||
|
w_ok = bool(r_ok and j.get("files"))
|
||||||
|
|
||||||
|
if not r_ok or w_ok != (p in ["a"]):
|
||||||
|
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
if (tag and p != "a") or (not tag and p == "a"):
|
||||||
|
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
||||||
|
|
||||||
|
if tag is not None and tag != "48x32":
|
||||||
|
raise Exception("tag [{}] at {}".format(tag, u))
|
||||||
|
|
||||||
|
cpp.stop(True)
|
||||||
|
|
||||||
|
|
||||||
|
def run(tc):
|
||||||
|
try:
|
||||||
|
tc()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
CPP[0].stop(False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
run(tc1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -37,6 +37,9 @@ class Cfg(Namespace):
|
|||||||
nih=True,
|
nih=True,
|
||||||
mtp=[],
|
mtp=[],
|
||||||
mte="a",
|
mte="a",
|
||||||
|
hist=None,
|
||||||
|
no_hash=False,
|
||||||
|
css_browser=None,
|
||||||
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,7 +102,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
pprint.pprint(vcfg)
|
pprint.pprint(vcfg)
|
||||||
|
|
||||||
self.args = Cfg(v=vcfg, a=["o:o", "x:x"])
|
self.args = Cfg(v=vcfg, a=["o:o", "x:x"])
|
||||||
self.auth = AuthSrv(self.args, self.log)
|
self.asrv = AuthSrv(self.args, self.log)
|
||||||
vfiles = [x for x in allfiles if x.startswith(top)]
|
vfiles = [x for x in allfiles if x.startswith(top)]
|
||||||
for fp in vfiles:
|
for fp in vfiles:
|
||||||
rok, wok = self.can_rw(fp)
|
rok, wok = self.can_rw(fp)
|
||||||
@@ -188,12 +191,12 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
def put(self, url):
|
def put(self, url):
|
||||||
buf = "PUT /{0} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\nContent-Length: {1}\r\n\r\nok {0}\n"
|
buf = "PUT /{0} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\nContent-Length: {1}\r\n\r\nok {0}\n"
|
||||||
buf = buf.format(url, len(url) + 4).encode("utf-8")
|
buf = buf.format(url, len(url) + 4).encode("utf-8")
|
||||||
conn = tu.VHttpConn(self.args, self.auth, self.log, buf)
|
conn = tu.VHttpConn(self.args, self.asrv, self.log, buf)
|
||||||
HttpCli(conn).run()
|
HttpCli(conn).run()
|
||||||
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||||
|
|
||||||
def curl(self, url, binary=False):
|
def curl(self, url, binary=False):
|
||||||
conn = tu.VHttpConn(self.args, self.auth, self.log, hdr(url))
|
conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url))
|
||||||
HttpCli(conn).run()
|
HttpCli(conn).run()
|
||||||
if binary:
|
if binary:
|
||||||
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
|
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
|
||||||
|
|||||||
@@ -18,8 +18,14 @@ from copyparty import util
|
|||||||
class Cfg(Namespace):
|
class Cfg(Namespace):
|
||||||
def __init__(self, a=[], v=[], c=None):
|
def __init__(self, a=[], v=[], c=None):
|
||||||
ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
||||||
ex["mtp"] = []
|
ex2 = {
|
||||||
ex["mte"] = "a"
|
"mtp": [],
|
||||||
|
"mte": "a",
|
||||||
|
"hist": None,
|
||||||
|
"no_hash": False,
|
||||||
|
"css_browser": None,
|
||||||
|
}
|
||||||
|
ex.update(ex2)
|
||||||
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)
|
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)
|
||||||
|
|
||||||
|
|
||||||
@@ -113,13 +119,13 @@ class TestVFS(unittest.TestCase):
|
|||||||
n = vfs.nodes["a"]
|
n = vfs.nodes["a"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a")
|
self.assertEqual(n.vpath, "a")
|
||||||
self.assertEqual(n.realpath, td + "/a")
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertEqual(n.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertEqual(n.uwrite, ["k"])
|
||||||
n = n.nodes["ac"]
|
n = n.nodes["ac"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a/ac")
|
self.assertEqual(n.vpath, "a/ac")
|
||||||
self.assertEqual(n.realpath, td + "/a/ac")
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertEqual(n.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertEqual(n.uwrite, ["k"])
|
||||||
n = n.nodes["acb"]
|
n = n.nodes["acb"]
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def get_ramdisk():
|
|||||||
|
|
||||||
if os.path.exists("/Volumes"):
|
if os.path.exists("/Volumes"):
|
||||||
# hdiutil eject /Volumes/cptd/
|
# hdiutil eject /Volumes/cptd/
|
||||||
devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://65536")
|
devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://131072")
|
||||||
devname = devname.strip()
|
devname = devname.strip()
|
||||||
print("devname: [{}]".format(devname))
|
print("devname: [{}]".format(devname))
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
@@ -110,12 +110,12 @@ class VHttpSrv(object):
|
|||||||
|
|
||||||
|
|
||||||
class VHttpConn(object):
|
class VHttpConn(object):
|
||||||
def __init__(self, args, auth, log, buf):
|
def __init__(self, args, asrv, log, buf):
|
||||||
self.s = VSock(buf)
|
self.s = VSock(buf)
|
||||||
self.sr = Unrecv(self.s)
|
self.sr = Unrecv(self.s)
|
||||||
self.addr = ("127.0.0.1", "42069")
|
self.addr = ("127.0.0.1", "42069")
|
||||||
self.args = args
|
self.args = args
|
||||||
self.auth = auth
|
self.asrv = asrv
|
||||||
self.is_mp = False
|
self.is_mp = False
|
||||||
self.log_func = log
|
self.log_func = log
|
||||||
self.log_src = "a"
|
self.log_src = "a"
|
||||||
|
|||||||
Reference in New Issue
Block a user