html-previewer 0.1.1__tar.gz
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.
- html_previewer-0.1.1/LICENSE +21 -0
- html_previewer-0.1.1/PKG-INFO +119 -0
- html_previewer-0.1.1/README.md +87 -0
- html_previewer-0.1.1/pyproject.toml +49 -0
- html_previewer-0.1.1/setup.cfg +4 -0
- html_previewer-0.1.1/src/html_previewer/__init__.py +15 -0
- html_previewer-0.1.1/src/html_previewer/__main__.py +6 -0
- html_previewer-0.1.1/src/html_previewer/cli.py +290 -0
- html_previewer-0.1.1/src/html_previewer/client.py +239 -0
- html_previewer-0.1.1/src/html_previewer/config.py +117 -0
- html_previewer-0.1.1/src/html_previewer.egg-info/PKG-INFO +119 -0
- html_previewer-0.1.1/src/html_previewer.egg-info/SOURCES.txt +16 -0
- html_previewer-0.1.1/src/html_previewer.egg-info/dependency_links.txt +1 -0
- html_previewer-0.1.1/src/html_previewer.egg-info/entry_points.txt +3 -0
- html_previewer-0.1.1/src/html_previewer.egg-info/requires.txt +3 -0
- html_previewer-0.1.1/src/html_previewer.egg-info/top_level.txt +1 -0
- html_previewer-0.1.1/tests/test_e2e.py +304 -0
- html_previewer-0.1.1/tests/test_helpers.py +163 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agony
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: html-previewer
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Cross-platform client (Windows/Linux/macOS) for html-previewer: publish short-lived HTML previews behind unguessable URLs
|
|
5
|
+
Author: agony
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Agony5757/html-previewer
|
|
8
|
+
Project-URL: Source, https://github.com/Agony5757/html-previewer
|
|
9
|
+
Project-URL: Issues, https://github.com/Agony5757/html-previewer/issues
|
|
10
|
+
Keywords: html,preview,publish,static-site,ephemeral,hosting
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == "test"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# html-previewer (Python client)
|
|
34
|
+
|
|
35
|
+
Cross-platform command-line client and library for
|
|
36
|
+
[html-previewer](https://github.com/Agony5757/html-previewer) — a self-hosted
|
|
37
|
+
service that publishes short-lived HTML previews behind **unguessable
|
|
38
|
+
capability URLs**, with automatic expiry and disk reclaim.
|
|
39
|
+
|
|
40
|
+
Works on **Linux, macOS and Windows**. Pure **stdlib** — zero runtime
|
|
41
|
+
dependencies. It is a faithful, feature-complete port of the classic
|
|
42
|
+
`preview-publish.sh`, so existing habits and scripts carry over.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install html-previewer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This installs the `preview-publish` command (alias: `html-previewer`).
|
|
51
|
+
|
|
52
|
+
## Configure
|
|
53
|
+
|
|
54
|
+
Point the client at your server once:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
preview-publish --base https://preview.example.com --key YOUR_API_KEY --save-config
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or just export the variables: `PREVIEW_API_BASE` and `PREVIEW_API_KEY`
|
|
61
|
+
(equivalently, put `PREVIEW_API_KEY=…` lines in the config file —
|
|
62
|
+
`~/.config/preview/config` on Linux/macOS, `%APPDATA%\preview\config` on
|
|
63
|
+
Windows; the same shell-style file the bash client reads).
|
|
64
|
+
|
|
65
|
+
## Use
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
preview-publish ./site/ my-site 14 # publish a folder, 14-day expiry
|
|
69
|
+
preview-publish ./report.zip # publish a zip (topic from filename)
|
|
70
|
+
preview-publish ./page.html # publish a single html page
|
|
71
|
+
preview-publish ./site/ my-site 2m # short-lived: 2 minutes
|
|
72
|
+
preview-publish --dry-run ./site/ my # predict: update in place, or new URL?
|
|
73
|
+
preview-publish --new ./site/ my-site # force a brand-new URL
|
|
74
|
+
preview-publish --list # list live previews (--list-all: everything)
|
|
75
|
+
preview-publish --info <hash> # one preview's record
|
|
76
|
+
preview-publish --delete <hash> # reclaim now
|
|
77
|
+
preview-publish --expiry <hash> --action extend --days 30
|
|
78
|
+
preview-publish --open ./site/ my-site # publish, then open in a browser
|
|
79
|
+
preview-publish --health # is the server alive?
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Re-publishing under a `topic` that is still live **updates it in place and
|
|
83
|
+
keeps the same URL** (expiry resets) — share a link once, keep refreshing the
|
|
84
|
+
content. A topic whose preview already expired gets a brand-new URL. Add
|
|
85
|
+
`--json` to any command for machine-readable output.
|
|
86
|
+
|
|
87
|
+
## Library
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from html_previewer import PreviewClient
|
|
91
|
+
|
|
92
|
+
c = PreviewClient("https://preview.example.com", api_key="…")
|
|
93
|
+
res = c.publish("./site", topic="my-site", expiry="14") # expiry: "14" or "2m"
|
|
94
|
+
print(res["url"])
|
|
95
|
+
|
|
96
|
+
for p in c.list_previews():
|
|
97
|
+
print(p["topic"], p["url"], p["seconds_remaining"])
|
|
98
|
+
|
|
99
|
+
c.check("my-site") # dry-run: update in place vs new URL
|
|
100
|
+
c.info(res["hash"])
|
|
101
|
+
c.expiry(res["hash"], "extend", days=30) # extend | expire | renew
|
|
102
|
+
c.delete(res["hash"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cd client
|
|
109
|
+
python -m pip install -e .[test]
|
|
110
|
+
python -m pytest tests -v
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The test suite includes an end-to-end test that boots the real server from
|
|
114
|
+
the repository checkout; it is skipped automatically when the server sources
|
|
115
|
+
are absent (e.g. in the sdist).
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# html-previewer (Python client)
|
|
2
|
+
|
|
3
|
+
Cross-platform command-line client and library for
|
|
4
|
+
[html-previewer](https://github.com/Agony5757/html-previewer) — a self-hosted
|
|
5
|
+
service that publishes short-lived HTML previews behind **unguessable
|
|
6
|
+
capability URLs**, with automatic expiry and disk reclaim.
|
|
7
|
+
|
|
8
|
+
Works on **Linux, macOS and Windows**. Pure **stdlib** — zero runtime
|
|
9
|
+
dependencies. It is a faithful, feature-complete port of the classic
|
|
10
|
+
`preview-publish.sh`, so existing habits and scripts carry over.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install html-previewer
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This installs the `preview-publish` command (alias: `html-previewer`).
|
|
19
|
+
|
|
20
|
+
## Configure
|
|
21
|
+
|
|
22
|
+
Point the client at your server once:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
preview-publish --base https://preview.example.com --key YOUR_API_KEY --save-config
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or just export the variables: `PREVIEW_API_BASE` and `PREVIEW_API_KEY`
|
|
29
|
+
(equivalently, put `PREVIEW_API_KEY=…` lines in the config file —
|
|
30
|
+
`~/.config/preview/config` on Linux/macOS, `%APPDATA%\preview\config` on
|
|
31
|
+
Windows; the same shell-style file the bash client reads).
|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
preview-publish ./site/ my-site 14 # publish a folder, 14-day expiry
|
|
37
|
+
preview-publish ./report.zip # publish a zip (topic from filename)
|
|
38
|
+
preview-publish ./page.html # publish a single html page
|
|
39
|
+
preview-publish ./site/ my-site 2m # short-lived: 2 minutes
|
|
40
|
+
preview-publish --dry-run ./site/ my # predict: update in place, or new URL?
|
|
41
|
+
preview-publish --new ./site/ my-site # force a brand-new URL
|
|
42
|
+
preview-publish --list # list live previews (--list-all: everything)
|
|
43
|
+
preview-publish --info <hash> # one preview's record
|
|
44
|
+
preview-publish --delete <hash> # reclaim now
|
|
45
|
+
preview-publish --expiry <hash> --action extend --days 30
|
|
46
|
+
preview-publish --open ./site/ my-site # publish, then open in a browser
|
|
47
|
+
preview-publish --health # is the server alive?
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Re-publishing under a `topic` that is still live **updates it in place and
|
|
51
|
+
keeps the same URL** (expiry resets) — share a link once, keep refreshing the
|
|
52
|
+
content. A topic whose preview already expired gets a brand-new URL. Add
|
|
53
|
+
`--json` to any command for machine-readable output.
|
|
54
|
+
|
|
55
|
+
## Library
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from html_previewer import PreviewClient
|
|
59
|
+
|
|
60
|
+
c = PreviewClient("https://preview.example.com", api_key="…")
|
|
61
|
+
res = c.publish("./site", topic="my-site", expiry="14") # expiry: "14" or "2m"
|
|
62
|
+
print(res["url"])
|
|
63
|
+
|
|
64
|
+
for p in c.list_previews():
|
|
65
|
+
print(p["topic"], p["url"], p["seconds_remaining"])
|
|
66
|
+
|
|
67
|
+
c.check("my-site") # dry-run: update in place vs new URL
|
|
68
|
+
c.info(res["hash"])
|
|
69
|
+
c.expiry(res["hash"], "extend", days=30) # extend | expire | renew
|
|
70
|
+
c.delete(res["hash"])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd client
|
|
77
|
+
python -m pip install -e .[test]
|
|
78
|
+
python -m pytest tests -v
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The test suite includes an end-to-end test that boots the real server from
|
|
82
|
+
the repository checkout; it is skipped automatically when the server sources
|
|
83
|
+
are absent (e.g. in the sdist).
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "html-previewer"
|
|
7
|
+
description = "Cross-platform client (Windows/Linux/macOS) for html-previewer: publish short-lived HTML previews behind unguessable URLs"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "agony" }]
|
|
13
|
+
keywords = ["html", "preview", "publish", "static-site", "ephemeral", "hosting"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP :: Site Management",
|
|
29
|
+
"Topic :: Utilities",
|
|
30
|
+
]
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
test = ["pytest"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
preview-publish = "html_previewer.cli:main"
|
|
38
|
+
html-previewer = "html_previewer.cli:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/Agony5757/html-previewer"
|
|
42
|
+
Source = "https://github.com/Agony5757/html-previewer"
|
|
43
|
+
Issues = "https://github.com/Agony5757/html-previewer/issues"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.dynamic]
|
|
49
|
+
version = { attr = "html_previewer.__version__" }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Cross-platform client for the html-previewer ephemeral preview service."""
|
|
2
|
+
from .client import (PreviewClient, PreviewError, build_zip, default_topic,
|
|
3
|
+
parse_expiry, slugify_topic)
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.1"
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"PreviewClient",
|
|
9
|
+
"PreviewError",
|
|
10
|
+
"build_zip",
|
|
11
|
+
"default_topic",
|
|
12
|
+
"parse_expiry",
|
|
13
|
+
"slugify_topic",
|
|
14
|
+
"__version__",
|
|
15
|
+
]
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Command-line interface: the ``preview-publish`` command.
|
|
2
|
+
|
|
3
|
+
Mirrors the interface of the classic ``client/preview-publish.sh`` so muscle
|
|
4
|
+
memory carries over, and adds the remaining API endpoints (--info, --expiry,
|
|
5
|
+
--health, --save-config) plus --open and --json.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import webbrowser
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from .client import PreviewClient, PreviewError, parse_expiry
|
|
19
|
+
from .config import resolve, save_config
|
|
20
|
+
|
|
21
|
+
PROG = "preview-publish"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _print_json(payload) -> None:
|
|
25
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _fmt_size(n) -> str:
|
|
29
|
+
try:
|
|
30
|
+
n = float(n)
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
return "?"
|
|
33
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
34
|
+
if n < 1024 or unit == "TB":
|
|
35
|
+
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
|
36
|
+
n /= 1024
|
|
37
|
+
return "?"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fmt_span(seconds) -> str:
|
|
41
|
+
try:
|
|
42
|
+
seconds = int(seconds)
|
|
43
|
+
except (TypeError, ValueError):
|
|
44
|
+
return "?"
|
|
45
|
+
for size, name in ((86400, "day"), (3600, "hour"), (60, "minute")):
|
|
46
|
+
if seconds >= size:
|
|
47
|
+
n = seconds // size
|
|
48
|
+
return f"{n} {name}{'s' if n != 1 else ''}"
|
|
49
|
+
return f"{seconds} seconds"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fmt_remaining(seconds) -> str:
|
|
53
|
+
try:
|
|
54
|
+
seconds = int(seconds)
|
|
55
|
+
except (TypeError, ValueError):
|
|
56
|
+
return "?"
|
|
57
|
+
if seconds <= 0:
|
|
58
|
+
return "expired"
|
|
59
|
+
days, rem = divmod(seconds, 86400)
|
|
60
|
+
hours, rem = divmod(rem, 3600)
|
|
61
|
+
minutes = rem // 60
|
|
62
|
+
if days:
|
|
63
|
+
return f"{days}d {hours}h left"
|
|
64
|
+
if hours:
|
|
65
|
+
return f"{hours}h {minutes}m left"
|
|
66
|
+
return f"{minutes}m left"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _fmt_ts(ts) -> str:
|
|
70
|
+
try:
|
|
71
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(ts)))
|
|
72
|
+
except (TypeError, ValueError, OSError):
|
|
73
|
+
return "?"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
77
|
+
epilog = """\
|
|
78
|
+
expiry formats:
|
|
79
|
+
7 7 days
|
|
80
|
+
2m 2 minutes (also Ns / Nh / Nd)
|
|
81
|
+
|
|
82
|
+
examples:
|
|
83
|
+
preview-publish ./site/ my-site 14
|
|
84
|
+
preview-publish ./page.html
|
|
85
|
+
preview-publish --dry-run ./site/ my-site
|
|
86
|
+
preview-publish --list
|
|
87
|
+
preview-publish --delete <hash>
|
|
88
|
+
preview-publish --expiry <hash> --action extend --days 30
|
|
89
|
+
"""
|
|
90
|
+
ap = argparse.ArgumentParser(
|
|
91
|
+
prog=PROG,
|
|
92
|
+
description="Publish short-lived HTML previews to a html-previewer service.",
|
|
93
|
+
epilog=epilog,
|
|
94
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
95
|
+
)
|
|
96
|
+
ap.add_argument("path", nargs="?", help="folder, .zip, or single .html to publish")
|
|
97
|
+
ap.add_argument("topic", nargs="?", help="URL label (default: derived from path)")
|
|
98
|
+
ap.add_argument("expiry_spec", nargs="?", metavar="EXPIRY",
|
|
99
|
+
help="N days, or N{s,m,h,d} e.g. 2m (default: server default)")
|
|
100
|
+
ap.add_argument("--version", action="version", version=f"{PROG} {__version__}")
|
|
101
|
+
|
|
102
|
+
g = ap.add_argument_group("actions (instead of publishing)")
|
|
103
|
+
g.add_argument("--dry-run", action="store_true",
|
|
104
|
+
help="predict update-in-place vs new URL (no upload)")
|
|
105
|
+
g.add_argument("--list", action="store_true", help="list live previews")
|
|
106
|
+
g.add_argument("--list-all", action="store_true",
|
|
107
|
+
help="list all previews incl. expired/deleted")
|
|
108
|
+
g.add_argument("--info", metavar="HASH", help="show one preview's record")
|
|
109
|
+
g.add_argument("--delete", metavar="HASH", help="reclaim a preview now")
|
|
110
|
+
g.add_argument("--expiry", metavar="HASH",
|
|
111
|
+
help="admin expiry control (requires --action)")
|
|
112
|
+
g.add_argument("--action", choices=("extend", "expire", "renew"),
|
|
113
|
+
help="expiry action for --expiry (days via --days)")
|
|
114
|
+
g.add_argument("--days", type=int, metavar="N",
|
|
115
|
+
help="days for --action extend/renew (default: 30/7)")
|
|
116
|
+
g.add_argument("--health", action="store_true", help="check server liveness")
|
|
117
|
+
g.add_argument("--save-config", action="store_true",
|
|
118
|
+
help="save --base/--key (or env) to the config file and exit")
|
|
119
|
+
|
|
120
|
+
g = ap.add_argument_group("options")
|
|
121
|
+
g.add_argument("--new", action="store_true",
|
|
122
|
+
default=os.environ.get("PREVIEW_NEW") == "1",
|
|
123
|
+
help="force a brand-new URL instead of same-name update "
|
|
124
|
+
"(env: PREVIEW_NEW=1)")
|
|
125
|
+
g.add_argument("--open", action="store_true",
|
|
126
|
+
help="open the published URL in a browser")
|
|
127
|
+
g.add_argument("--json", action="store_true", dest="as_json",
|
|
128
|
+
help="print raw JSON responses")
|
|
129
|
+
g.add_argument("--base", metavar="URL", help="API base URL (env: PREVIEW_API_BASE)")
|
|
130
|
+
g.add_argument("--key", metavar="KEY", help="API key (env: PREVIEW_API_KEY)")
|
|
131
|
+
g.add_argument("--config", metavar="FILE",
|
|
132
|
+
help="config file path (default: platform config dir)")
|
|
133
|
+
return ap
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---- output -------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def _print_publish(res: dict) -> None:
|
|
139
|
+
action = "updated in place" if res.get("updated") else "created"
|
|
140
|
+
print(f"published topic={res.get('topic')} ({action})")
|
|
141
|
+
print(f" url: {res.get('url')}")
|
|
142
|
+
print(f" expires: {_fmt_ts(res.get('expires_at'))} "
|
|
143
|
+
f"(in {_fmt_span(res.get('ttl_seconds'))})")
|
|
144
|
+
print(f" size: {_fmt_size(res.get('size_bytes'))}, "
|
|
145
|
+
f"{res.get('file_count', '?')} files")
|
|
146
|
+
if not res.get("has_index_html"):
|
|
147
|
+
note = ("auto-generated directory index" if res.get("auto_index")
|
|
148
|
+
else "WARNING: no index.html")
|
|
149
|
+
print(f" index: {note}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _print_check(res: dict) -> None:
|
|
153
|
+
print(f"dry-run: topic={res.get('topic')}")
|
|
154
|
+
if res.get("action") == "update":
|
|
155
|
+
print(f" would UPDATE in place (same URL): {res.get('url')}")
|
|
156
|
+
else:
|
|
157
|
+
print(" would CREATE a new URL")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _print_list(previews: list) -> None:
|
|
161
|
+
if not previews:
|
|
162
|
+
print("no previews")
|
|
163
|
+
return
|
|
164
|
+
for p in previews:
|
|
165
|
+
print(f"{p.get('hash')} {p.get('topic')} {p.get('status')}"
|
|
166
|
+
f" ({_fmt_remaining(p.get('seconds_remaining'))},"
|
|
167
|
+
f" {_fmt_size(p.get('size_bytes'))},"
|
|
168
|
+
f" {p.get('file_count', '?')} files)")
|
|
169
|
+
print(f" {p.get('url')}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _print_preview(p: dict, title: str) -> None:
|
|
173
|
+
print(title)
|
|
174
|
+
for k in ("hash", "topic", "url", "status", "days", "size_bytes",
|
|
175
|
+
"file_count", "archived"):
|
|
176
|
+
if k in p:
|
|
177
|
+
print(f" {k + ':':12} {p[k]}")
|
|
178
|
+
print(f" expires: {_fmt_ts(p.get('expires_at'))} "
|
|
179
|
+
f"({_fmt_remaining(p.get('seconds_remaining'))})")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---- command ------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def main(argv: list[str] | None = None) -> int:
|
|
185
|
+
args = _build_parser().parse_args(argv)
|
|
186
|
+
|
|
187
|
+
if args.expiry and not args.action:
|
|
188
|
+
print(f"{PROG}: --expiry requires --action extend|expire|renew",
|
|
189
|
+
file=sys.stderr)
|
|
190
|
+
return 2
|
|
191
|
+
if args.action and not args.expiry:
|
|
192
|
+
print(f"{PROG}: --action requires --expiry HASH", file=sys.stderr)
|
|
193
|
+
return 2
|
|
194
|
+
|
|
195
|
+
cfg = resolve(base=args.base, key=args.key, config_path=args.config)
|
|
196
|
+
|
|
197
|
+
if args.save_config:
|
|
198
|
+
path = save_config(base=cfg["base"], key=cfg["key"],
|
|
199
|
+
path=cfg["config_path"])
|
|
200
|
+
print(f"saved config to {path}")
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
client = PreviewClient(cfg["base"], api_key=cfg["key"])
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
if args.health:
|
|
207
|
+
res = client.health()
|
|
208
|
+
if args.as_json:
|
|
209
|
+
_print_json(res)
|
|
210
|
+
else:
|
|
211
|
+
print(f"ok: {cfg['base']} is alive")
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
if args.list or args.list_all:
|
|
215
|
+
previews = client.list_previews(all=args.list_all)
|
|
216
|
+
if args.as_json:
|
|
217
|
+
_print_json({"previews": previews})
|
|
218
|
+
else:
|
|
219
|
+
_print_list(previews)
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
if args.info:
|
|
223
|
+
res = client.info(args.info)
|
|
224
|
+
if args.as_json:
|
|
225
|
+
_print_json(res)
|
|
226
|
+
else:
|
|
227
|
+
_print_preview(res.get("preview", {}), f"preview {args.info}")
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
if args.delete:
|
|
231
|
+
res = client.delete(args.delete)
|
|
232
|
+
if args.as_json:
|
|
233
|
+
_print_json(res)
|
|
234
|
+
else:
|
|
235
|
+
print(f"deleted {res.get('hash')} "
|
|
236
|
+
f"(live copy reclaimed; git archive kept)")
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
if args.expiry:
|
|
240
|
+
res = client.expiry(args.expiry, args.action, days=args.days)
|
|
241
|
+
if args.as_json:
|
|
242
|
+
_print_json(res)
|
|
243
|
+
else:
|
|
244
|
+
_print_preview(res.get("preview", {}),
|
|
245
|
+
f"{args.action}: preview {args.expiry}")
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
# default action: publish (or dry-run)
|
|
249
|
+
if not args.path:
|
|
250
|
+
print(f"{PROG}: a path is required to publish "
|
|
251
|
+
f"(or use --list/--delete/--info/...)", file=sys.stderr)
|
|
252
|
+
return 2
|
|
253
|
+
path = Path(args.path)
|
|
254
|
+
if not path.exists():
|
|
255
|
+
print(f"{PROG}: no such file or directory: {path}", file=sys.stderr)
|
|
256
|
+
return 1
|
|
257
|
+
|
|
258
|
+
if args.dry_run:
|
|
259
|
+
res = client.publish(path, topic=args.topic, dry_run=True,
|
|
260
|
+
new=args.new)
|
|
261
|
+
if args.as_json:
|
|
262
|
+
_print_json(res)
|
|
263
|
+
else:
|
|
264
|
+
_print_check(res)
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
parse_expiry(args.expiry_spec) # fail fast on a bad spec, before zipping
|
|
268
|
+
res = client.publish(path, topic=args.topic, expiry=args.expiry_spec,
|
|
269
|
+
new=args.new)
|
|
270
|
+
if args.as_json:
|
|
271
|
+
_print_json(res)
|
|
272
|
+
else:
|
|
273
|
+
_print_publish(res)
|
|
274
|
+
if args.open and res.get("url"):
|
|
275
|
+
webbrowser.open(res["url"])
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
except PreviewError as e:
|
|
279
|
+
print(f"{PROG}: {e}", file=sys.stderr)
|
|
280
|
+
return 1
|
|
281
|
+
except ValueError as e:
|
|
282
|
+
print(f"{PROG}: {e}", file=sys.stderr)
|
|
283
|
+
return 2
|
|
284
|
+
except KeyboardInterrupt:
|
|
285
|
+
print(f"{PROG}: interrupted", file=sys.stderr)
|
|
286
|
+
return 130
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
sys.exit(main())
|