droppage 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- package/dist/cli.js +78 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,18 +38,18 @@ Open the printed link in any browser. That's it.
|
|
|
38
38
|
|---|---|
|
|
39
39
|
| `droppage login` | Authenticate with a magic link (session stored in `~/.droppage/config.json`). |
|
|
40
40
|
| `droppage logout` | Sign out and clear local credentials. |
|
|
41
|
-
| `droppage upload <file>` | Publish an `.html`/`.htm` file (and the local images it references) and print its URL. Options: `--password`, `--email <addr>`, `--private`, `--expires <when>`. |
|
|
42
|
-
| `droppage update <slug> <file>` | Replace the HTML (and images) behind an existing URL. Keeps the URL, access mode, and stats. `--expires <when\|never>` changes the expiry. |
|
|
41
|
+
| `droppage upload <file>` | Publish an `.html`/`.htm` file (and the local images and videos it references) and print its URL. Options: `--password`, `--email <addr>`, `--private`, `--expires <when>`. |
|
|
42
|
+
| `droppage update <slug> <file>` | Replace the HTML (and images and videos) behind an existing URL. Keeps the URL, access mode, and stats. `--expires <when\|never>` changes the expiry. |
|
|
43
43
|
| `droppage expire <slug> <when\|never>` | Set or clear when a page expires without re-uploading. |
|
|
44
44
|
| `droppage list` | List your pages, including an EXPIRES column. `--json` for machines (rows have `expires_at`), `--preview` for no-count preview URLs. |
|
|
45
|
-
| `droppage delete <slug>` | Delete a page and its images. `--yes` skips the confirmation prompt. |
|
|
45
|
+
| `droppage delete <slug>` | Delete a page and its images and videos. `--yes` skips the confirmation prompt. |
|
|
46
46
|
| `droppage recipients list <slug>` | List recipients of an email-gated page (`--json`, `--page <n>`). |
|
|
47
47
|
| `droppage recipients add <slug> <emails...>` | Add recipients and queue their invitation emails. |
|
|
48
48
|
| `droppage recipients remove <slug> <email>` | Revoke a recipient; their invitation link stops working (`--yes`). |
|
|
49
49
|
| `droppage recipients resend <slug> <email>` | Queue a fresh invitation for an existing recipient. |
|
|
50
|
-
| `droppage image upload <files...> --page <slug>` | Advanced: add images (PNG, JPEG, GIF, WebP, AVIF, SVG; up to 4 MB each) to an existing page and print their URLs. `--json` prints `[{file,id,url,bytes,content_type,existing}]`. |
|
|
51
|
-
| `droppage image list [--page <slug>]` | List your pages' images; with `--page`, one page and its image budget. `--json` for machines. |
|
|
52
|
-
| `droppage image delete <id-or-url...>` | Delete images. They stop being served within 5 minutes; a browser that already loaded one may keep it up to a day. |
|
|
50
|
+
| `droppage image upload <files...> --page <slug>` | Advanced: add images (PNG, JPEG, GIF, WebP, AVIF, SVG; up to 4 MB each) or videos (MP4, WebM; up to 20 MB each) to an existing page and print their URLs. Alias: `droppage media upload`. `--json` prints `[{file,id,url,bytes,content_type,existing}]`. |
|
|
51
|
+
| `droppage image list [--page <slug>]` | List your pages' images and videos; with `--page`, one page and its image budget. `--json` for machines. |
|
|
52
|
+
| `droppage image delete <id-or-url...>` | Delete images or videos. They stop being served within 5 minutes; a browser that already loaded one may keep it up to a day. |
|
|
53
53
|
| `droppage quota` | Show the current plan, page usage, monthly meter, reset date, and enabled features. `--json` returns the account API body. |
|
|
54
54
|
| `droppage feedback <message>` | Send feedback without requiring login. Options: `--kind idea|bug|review|other`, `--agent [model]`, `--contact <email>`, review-only `--rating <1-5>` and `--allow-quote`. Quote permission never publishes automatically. |
|
|
55
55
|
| `droppage upgrade` | Install the latest CLI globally with npm and report the installed version. |
|
|
@@ -120,6 +120,38 @@ only the page URL.
|
|
|
120
120
|
`{images:[{id,url,ext,content_type,bytes,name,created_at,page}], page?:{slug,used_bytes,limit_bytes}}`);
|
|
121
121
|
`droppage image delete <id-or-url...>` removes them.
|
|
122
122
|
|
|
123
|
+
## Videos
|
|
124
|
+
|
|
125
|
+
Short product films (CLI 0.3.0+) are hosted exactly like images. Reference local MP4 or WebM files,
|
|
126
|
+
and a poster image, in a `<video>`:
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<video autoplay muted loop playsinline poster="still.webp">
|
|
130
|
+
<source src="film.webm" type="video/webm">
|
|
131
|
+
<source src="film.mp4" type="video/mp4">
|
|
132
|
+
</video>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
droppage upload page.html
|
|
137
|
+
# stderr: droppage: image still.webp → https://droppage.dev/i/AbCdEfGhIjKl.webp
|
|
138
|
+
# stderr: droppage: video film.webm → https://droppage.dev/i/MnOpQrStUvWx.webm
|
|
139
|
+
# stderr: droppage: video film.mp4 → https://droppage.dev/i/YzAbCdEfGhIj.mp4
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- Picked up: `<video src>`, `<source src>` (inside `<video>`, and `<picture>` as before), and
|
|
143
|
+
`<video poster>`. Same folder rule as images.
|
|
144
|
+
- Each video up to 20 MB (20,000,000 bytes), checked by its file signature, not its name: MP4
|
|
145
|
+
(H.264 plays everywhere) and WebM (VP9). `.mov` and `.mkv` are refused; convert them first, e.g.
|
|
146
|
+
`ffmpeg -i in.mov -c:v libx264 -pix_fmt yuv420p -movflags +faststart -an film.mp4`.
|
|
147
|
+
- Videos share the page's image budget (Free 5 MB, Starter 20 MB, Builder 50 MB per page), are
|
|
148
|
+
pruned by `update` and deleted with the page, like images. Video traffic counts toward your
|
|
149
|
+
monthly bandwidth.
|
|
150
|
+
- Served from `https://droppage.dev/i/<id>.mp4|webm` with HTTP Range support (`206 Partial Content`),
|
|
151
|
+
so Safari, iOS and seeking work. Public to anyone with the URL, like images.
|
|
152
|
+
- No need to inline films as base64 any more. `droppage image upload film.mp4 --page <slug>` (or
|
|
153
|
+
`droppage media upload`) adds a video to an existing page.
|
|
154
|
+
|
|
123
155
|
## Expiry
|
|
124
156
|
|
|
125
157
|
Pages can expire. Nothing expires unless you set it, on any plan.
|
|
@@ -159,9 +191,9 @@ team, a weekly KPI or stakeholder report, a client deliverable, a notebook expor
|
|
|
159
191
|
droppage keeps a strict stream contract so it is safe to call from automation:
|
|
160
192
|
|
|
161
193
|
- `upload` and `update` print **only the URL** on stdout, even when they upload images;
|
|
162
|
-
`image upload` prints **only the image URL(s)**; `expire` prints **nothing** on stdout.
|
|
194
|
+
`image upload` prints **only the image or video URL(s)**; `expire` prints **nothing** on stdout.
|
|
163
195
|
- `list --json`, `image list --json`, `recipients list --json`, and `quota --json` print **only JSON** on stdout.
|
|
164
|
-
- Progress, warnings, prompts, errors, and the per-
|
|
196
|
+
- Progress, warnings, prompts, errors, and the per-file `droppage: image … → …` / `droppage: video … → …` lines go to **stderr**.
|
|
165
197
|
- Non-zero exit code on any failure.
|
|
166
198
|
- Use `--yes` on `delete` / `recipients remove` for non-interactive runs.
|
|
167
199
|
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ var init_package = __esm({
|
|
|
15
15
|
"package.json"() {
|
|
16
16
|
package_default = {
|
|
17
17
|
name: "droppage",
|
|
18
|
-
version: "0.
|
|
18
|
+
version: "0.3.0",
|
|
19
19
|
description: "Publish HTML files as shareable, access-controlled live pages",
|
|
20
20
|
type: "module",
|
|
21
21
|
license: "Apache-2.0",
|
|
@@ -524,9 +524,11 @@ import { dirname, extname, relative, resolve, sep } from "path";
|
|
|
524
524
|
function imageSlots(html) {
|
|
525
525
|
const slots = [];
|
|
526
526
|
const attrValue = (match) => ({ start: match.index + match[1].length + 1, value: match[3] });
|
|
527
|
-
for (const
|
|
528
|
-
const
|
|
529
|
-
|
|
527
|
+
for (const re of [SRC_RE, POSTER_RE]) {
|
|
528
|
+
for (const match of html.matchAll(re)) {
|
|
529
|
+
const { start, value } = attrValue(match);
|
|
530
|
+
slots.push({ start, end: start + value.length, ref: value.trim() });
|
|
531
|
+
}
|
|
530
532
|
}
|
|
531
533
|
for (const match of html.matchAll(SRCSET_RE)) {
|
|
532
534
|
const { start, value } = attrValue(match);
|
|
@@ -612,7 +614,7 @@ function findLocalImages(html, htmlFile) {
|
|
|
612
614
|
} catch {
|
|
613
615
|
continue;
|
|
614
616
|
}
|
|
615
|
-
if (!
|
|
617
|
+
if (!MEDIA_EXTS.has(extname(decoded).toLowerCase())) continue;
|
|
616
618
|
const path = resolve(base, decoded);
|
|
617
619
|
if (!inside(base, path) || !existsSync(path)) continue;
|
|
618
620
|
let real;
|
|
@@ -637,19 +639,23 @@ function rewriteImageRefs(html, urls) {
|
|
|
637
639
|
}
|
|
638
640
|
return out + html.slice(last);
|
|
639
641
|
}
|
|
640
|
-
var IMAGE_EXTS,
|
|
642
|
+
var IMAGE_EXTS, VIDEO_EXTS, MEDIA_EXTS, attrRe, SRC_RE, SRCSET_RE, POSTER_RE, LINK_RE, ATTR_RE, ICON_RELS, STYLE_RE, CSS_URL_RE, isVideoFile;
|
|
641
643
|
var init_html_images = __esm({
|
|
642
644
|
"src/lib/html-images.ts"() {
|
|
643
645
|
"use strict";
|
|
644
646
|
IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg"]);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
647
|
+
VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".webm"]);
|
|
648
|
+
MEDIA_EXTS = /* @__PURE__ */ new Set([...IMAGE_EXTS, ...VIDEO_EXTS]);
|
|
649
|
+
attrRe = (tags, name) => new RegExp(`(<(?:${tags})\\b[^>]*?\\s${name}\\s*=\\s*)(["'])([^"']+)\\2`, "gi");
|
|
650
|
+
SRC_RE = attrRe("img|source|video", "src");
|
|
651
|
+
SRCSET_RE = attrRe("img|source", "srcset");
|
|
652
|
+
POSTER_RE = attrRe("video", "poster");
|
|
648
653
|
LINK_RE = /<link\b[^>]*>/gi;
|
|
649
654
|
ATTR_RE = /\s([a-z][a-z0-9-]*)\s*=\s*(?:(["'])([^"']*)\2|([^\s"'=<>`]+))/gi;
|
|
650
655
|
ICON_RELS = /* @__PURE__ */ new Set(["icon", "apple-touch-icon", "apple-touch-icon-precomposed"]);
|
|
651
656
|
STYLE_RE = /(<style\b[^>]*>)([\s\S]*?)<\/style\s*>/gi;
|
|
652
657
|
CSS_URL_RE = /\burl\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"'()]+))\s*\)/gi;
|
|
658
|
+
isVideoFile = (path) => VIDEO_EXTS.has(extname(path).toLowerCase());
|
|
653
659
|
}
|
|
654
660
|
});
|
|
655
661
|
|
|
@@ -699,10 +705,18 @@ async function call(path, init) {
|
|
|
699
705
|
}
|
|
700
706
|
return body;
|
|
701
707
|
}
|
|
708
|
+
function asUploaded(body) {
|
|
709
|
+
const image = body;
|
|
710
|
+
if (!image || typeof image.url !== "string" || typeof image.id !== "string") {
|
|
711
|
+
throw new ImageRequestError("unexpected response from the image API", 0);
|
|
712
|
+
}
|
|
713
|
+
return image;
|
|
714
|
+
}
|
|
702
715
|
async function uploadImage(file, slug, { update = false } = {}) {
|
|
703
716
|
const info = await stat(file).catch(() => null);
|
|
704
717
|
if (!info?.isFile()) throw new ImageRequestError(`file not found: ${file}`, 0);
|
|
705
718
|
if (info.size === 0) throw new ImageRequestError(`file is empty: ${file}`, 0);
|
|
719
|
+
if (isVideoFile(file)) return uploadVideo(file, info.size, slug, update);
|
|
706
720
|
if (info.size > MAX_IMAGE_BYTES) {
|
|
707
721
|
throw new ImageRequestError(`image is larger than ${MAX_IMAGE_BYTES / 1024 / 1024} MB (${info.size} bytes): ${file}`, 413);
|
|
708
722
|
}
|
|
@@ -721,11 +735,48 @@ async function uploadImage(file, slug, { update = false } = {}) {
|
|
|
721
735
|
if (info.size > HEAVY_IMAGE_BYTES && /\.(png|jpe?g|gif)$/i.test(file)) {
|
|
722
736
|
console.error(`droppage: tip: ${basename(file)} is ${Math.round(info.size / 1024)} KB; WebP or AVIF is usually several times smaller (SVG for charts).`);
|
|
723
737
|
}
|
|
724
|
-
|
|
725
|
-
|
|
738
|
+
return asUploaded(body);
|
|
739
|
+
}
|
|
740
|
+
async function uploadVideo(file, size, slug, update) {
|
|
741
|
+
if (size > MAX_VIDEO_BYTES) {
|
|
742
|
+
throw new ImageRequestError(`video is larger than ${MAX_VIDEO_BYTES / 1e6} MB (${size} bytes): ${file}`, 413);
|
|
743
|
+
}
|
|
744
|
+
const bytes = await readFile(file);
|
|
745
|
+
const token = await accessToken();
|
|
746
|
+
const started = await call("/api/images/uploads", {
|
|
747
|
+
method: "POST",
|
|
748
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
749
|
+
body: JSON.stringify({
|
|
750
|
+
page: slug,
|
|
751
|
+
bytes: bytes.length,
|
|
752
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
753
|
+
filename: basename(file),
|
|
754
|
+
head: bytes.subarray(0, 64).toString("base64"),
|
|
755
|
+
update
|
|
756
|
+
})
|
|
757
|
+
});
|
|
758
|
+
if (typeof started?.url === "string") return asUploaded(started);
|
|
759
|
+
if (!started?.upload_url || !started.complete_url || !started.content_type) {
|
|
726
760
|
throw new ImageRequestError("unexpected response from the image API", 0);
|
|
727
761
|
}
|
|
728
|
-
|
|
762
|
+
let stored;
|
|
763
|
+
try {
|
|
764
|
+
stored = await fetch(started.upload_url, {
|
|
765
|
+
method: "PUT",
|
|
766
|
+
headers: { "Content-Type": started.content_type, "x-upsert": "false" },
|
|
767
|
+
body: bytes
|
|
768
|
+
});
|
|
769
|
+
} catch (err) {
|
|
770
|
+
throw new ImageRequestError(`could not upload the video: ${err instanceof Error ? err.message : String(err)}`, 0);
|
|
771
|
+
}
|
|
772
|
+
if (!stored.ok) {
|
|
773
|
+
const detail = await stored.text().catch(() => "");
|
|
774
|
+
throw new ImageRequestError(`could not upload the video (storage answered HTTP ${stored.status}${detail ? `: ${detail.slice(0, 200)}` : ""})`, stored.status);
|
|
775
|
+
}
|
|
776
|
+
return asUploaded(await call(started.complete_url, {
|
|
777
|
+
method: "POST",
|
|
778
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
779
|
+
}));
|
|
729
780
|
}
|
|
730
781
|
async function listImages(slug) {
|
|
731
782
|
const query = slug ? `?page=${encodeURIComponent(slug)}` : "";
|
|
@@ -758,13 +809,15 @@ async function pruneImages(slug, html) {
|
|
|
758
809
|
});
|
|
759
810
|
return Number(body?.removed ?? 0);
|
|
760
811
|
}
|
|
761
|
-
var MAX_IMAGE_BYTES, HEAVY_IMAGE_BYTES, ImageRequestError;
|
|
812
|
+
var MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, HEAVY_IMAGE_BYTES, ImageRequestError;
|
|
762
813
|
var init_images = __esm({
|
|
763
814
|
"src/services/images.ts"() {
|
|
764
815
|
"use strict";
|
|
816
|
+
init_html_images();
|
|
765
817
|
init_share_url();
|
|
766
818
|
init_supabase();
|
|
767
819
|
MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
820
|
+
MAX_VIDEO_BYTES = 2e7;
|
|
768
821
|
HEAVY_IMAGE_BYTES = 300 * 1024;
|
|
769
822
|
ImageRequestError = class extends Error {
|
|
770
823
|
constructor(message, status) {
|
|
@@ -790,7 +843,8 @@ async function attachLocalImages(file, slug, { html, update = false } = {}) {
|
|
|
790
843
|
const result = await uploadImage(image.path, slug, { update });
|
|
791
844
|
urls.set(image.ref, result.url);
|
|
792
845
|
uploaded.push(result);
|
|
793
|
-
|
|
846
|
+
const kind = isVideoFile(image.path) ? "video" : "image";
|
|
847
|
+
console.error(`droppage: ${kind} ${basename2(image.path)} \u2192 ${result.url}`);
|
|
794
848
|
}
|
|
795
849
|
return { html: Buffer.from(rewriteImageRefs(text, urls), "utf8"), uploaded };
|
|
796
850
|
}
|
|
@@ -1021,7 +1075,7 @@ function registerUpload(program2) {
|
|
|
1021
1075
|
"--email <address>",
|
|
1022
1076
|
"restrict access to an email allowlist (repeatable; comma-separated)",
|
|
1023
1077
|
(val, prev = []) => prev.concat(val.split(","))
|
|
1024
|
-
).option("--private", "restrict access to only you (owner-only draft)").option("--expires <when>", "delete the page (and its images) after 30m, 12h, 7d, 2w or on a date").action(
|
|
1078
|
+
).option("--private", "restrict access to only you (owner-only draft)").option("--expires <when>", "delete the page (and its images and videos) after 30m, 12h, 7d, 2w or on a date").action(
|
|
1025
1079
|
async (file, options) => {
|
|
1026
1080
|
let user;
|
|
1027
1081
|
try {
|
|
@@ -1232,7 +1286,7 @@ var init_pages = __esm({
|
|
|
1232
1286
|
// src/commands/update.ts
|
|
1233
1287
|
import { readFile as readFile5 } from "fs/promises";
|
|
1234
1288
|
function registerUpdate(program2) {
|
|
1235
|
-
program2.command("update").description("Replace the HTML behind an existing page URL (its local images are uploaded too)").argument("<slug>", "slug of the page to update").argument("<file>", "path to replacement .html file").option("--expires <when>", "set when the page expires: 30m, 12h, 7d, 2w, a date, or never").action(async (slug, file, options) => {
|
|
1289
|
+
program2.command("update").description("Replace the HTML behind an existing page URL (its local images and videos are uploaded too)").argument("<slug>", "slug of the page to update").argument("<file>", "path to replacement .html file").option("--expires <when>", "set when the page expires: 30m, 12h, 7d, 2w, a date, or never").action(async (slug, file, options) => {
|
|
1236
1290
|
let user;
|
|
1237
1291
|
try {
|
|
1238
1292
|
user = await requireAuth();
|
|
@@ -1248,7 +1302,7 @@ function registerUpdate(program2) {
|
|
|
1248
1302
|
const { html } = await attachLocalImages(file, slug, { update: true });
|
|
1249
1303
|
const result = await updatePageHtml({ slug, file, user, html });
|
|
1250
1304
|
const removed = await pruneUnusedImages(slug, html);
|
|
1251
|
-
if (removed) console.error(`droppage: removed ${removed} image(s) the page no longer uses`);
|
|
1305
|
+
if (removed) console.error(`droppage: removed ${removed} image or video file(s) the page no longer uses`);
|
|
1252
1306
|
if (expiresAt !== void 0) {
|
|
1253
1307
|
await setPageExpiry(slug, expiresAt);
|
|
1254
1308
|
console.error(expiresAt ? `droppage: expires ${expiresAt.toISOString()}` : "droppage: never expires");
|
|
@@ -1264,7 +1318,7 @@ function registerUpdate(program2) {
|
|
|
1264
1318
|
}
|
|
1265
1319
|
}
|
|
1266
1320
|
});
|
|
1267
|
-
program2.command("expire").description("Set when a page (and its images) is deleted: 30m, 12h, 7d, 2w, a date, or never").argument("<slug>", "slug of the page").argument("<when>", "e.g. 7d, 2026-12-31, or never").action(async (slug, when) => {
|
|
1321
|
+
program2.command("expire").description("Set when a page (and its images and videos) is deleted: 30m, 12h, 7d, 2w, a date, or never").argument("<slug>", "slug of the page").argument("<when>", "e.g. 7d, 2026-12-31, or never").action(async (slug, when) => {
|
|
1268
1322
|
try {
|
|
1269
1323
|
await requireAuth();
|
|
1270
1324
|
} catch (err) {
|
|
@@ -1962,8 +2016,8 @@ function fail(err) {
|
|
|
1962
2016
|
process.exitCode = 1;
|
|
1963
2017
|
}
|
|
1964
2018
|
function registerImage(program2) {
|
|
1965
|
-
const image = program2.command("image").description("Images that belong to a page (uploaded automatically by upload/update from local files the HTML references)");
|
|
1966
|
-
image.command("upload").description("Add images to one of your pages and print their URLs (PNG, JPEG, GIF, WebP, AVIF, SVG
|
|
2019
|
+
const image = program2.command("image").alias("media").description("Images and videos that belong to a page (uploaded automatically by upload/update from local files the HTML references)");
|
|
2020
|
+
image.command("upload").description("Add images or videos to one of your pages and print their URLs (PNG, JPEG, GIF, WebP, AVIF, SVG up to 4 MB each; MP4, WebM up to 20 MB each)").argument("<files...>", "image or video files").requiredOption("--page <slug>", "the page the files belong to (they go when it is deleted or expires)").option("--json", "print the results as JSON").action(async (files, options) => {
|
|
1967
2021
|
if (!await signedIn()) return;
|
|
1968
2022
|
const results = [];
|
|
1969
2023
|
for (const file of files) {
|
|
@@ -1981,7 +2035,7 @@ function registerImage(program2) {
|
|
|
1981
2035
|
}
|
|
1982
2036
|
if (options.json) process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
1983
2037
|
});
|
|
1984
|
-
image.command("list").description("List your pages' images (one page with --page, including its
|
|
2038
|
+
image.command("list").description("List your pages' images and videos (one page with --page, including its media budget)").option("--page <slug>", "only this page").option("--json", "print the list as JSON").action(async (options) => {
|
|
1985
2039
|
if (!await signedIn()) return;
|
|
1986
2040
|
try {
|
|
1987
2041
|
const list = await listImages(options.page);
|
|
@@ -1990,7 +2044,7 @@ function registerImage(program2) {
|
|
|
1990
2044
|
return;
|
|
1991
2045
|
}
|
|
1992
2046
|
if (list.images.length === 0) {
|
|
1993
|
-
console.error("droppage: no images. `droppage upload page.html` uploads the
|
|
2047
|
+
console.error("droppage: no images or videos. `droppage upload page.html` uploads the ones the page uses.");
|
|
1994
2048
|
} else {
|
|
1995
2049
|
process.stdout.write(renderTable(
|
|
1996
2050
|
["ID", "PAGE", "NAME", "SIZE", "UPLOADED", "URL"],
|
|
@@ -2000,13 +2054,13 @@ function registerImage(program2) {
|
|
|
2000
2054
|
}
|
|
2001
2055
|
if (list.page) {
|
|
2002
2056
|
const limit = list.page.limit_bytes === null ? "unlimited" : formatBytes3(list.page.limit_bytes);
|
|
2003
|
-
console.error(`droppage: images on ${list.page.slug}: ${formatBytes3(list.page.used_bytes)} of ${limit} per page.`);
|
|
2057
|
+
console.error(`droppage: images and videos on ${list.page.slug}: ${formatBytes3(list.page.used_bytes)} of ${limit} per page.`);
|
|
2004
2058
|
}
|
|
2005
2059
|
} catch (err) {
|
|
2006
2060
|
fail(err);
|
|
2007
2061
|
}
|
|
2008
2062
|
});
|
|
2009
|
-
image.command("delete").description("Delete images (by id or URL); they stop being served within 5 minutes; a browser that already loaded one may keep it up to a day").argument("<images...>", "image ids or URLs").action(async (ids) => {
|
|
2063
|
+
image.command("delete").description("Delete images or videos (by id or URL); they stop being served within 5 minutes; a browser that already loaded one may keep it up to a day").argument("<images...>", "image or video ids or URLs").action(async (ids) => {
|
|
2010
2064
|
if (!await signedIn()) return;
|
|
2011
2065
|
for (const value of ids) {
|
|
2012
2066
|
try {
|