junk.py
download raw
#!/usr/bin/env -S uv -q run --script
"""
Upload files to a junk drawer
I use this like a substitute for gist on codeberg. It's not a one-to-one
replacement, but it does meet my needs. (embedding would be nice though)
It grabs the credentials from a tea or forgego-cli configuration file, so set
that up first!
"""
# SPDX-FileCopyrightText: 2025 Jeff Epler
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "requests",
# "platformdirs",
# "pyyaml",
# "click",
# ]
# ///
import binascii
import hashlib
import json
import pathlib
import random
import string
import sys
import typing
import urllib.parse
import click
import platformdirs
import requests
import yaml
base36_chars = string.ascii_lowercase + string.digits
def random_base36(n):
return "".join(random.choice(base36_chars) for _ in range(n))
def get_loader():
try:
return yaml.CLoader
except AttributeError:
return yaml.Loader
def yaml_load(stream):
return yaml.load(stream, Loader=get_loader())
def symlink_path(f: pathlib.Path) -> pathlib.Path:
f = f.absolute()
symlink_farm = platformdirs.user_cache_path("junk")
symlink_farm.mkdir(parents=True, exist_ok=True)
symlink_name = str(f).replace("%", "%%").replace("/", "%2f")
return symlink_farm/symlink_name
def find_path_from_file(f: pathlib.Path) -> str | None:
p = symlink_path(f)
if not p.is_symlink():
print(f"Did not find path for {f} via {p}")
return None
r = p.readlink()
print(f"Stored path for {f} is {r} via {p}")
return r
def store_path_from_file(f: pathlib.Path, path: str) -> None:
p = symlink_path(f)
tmp_p = p.with_suffix("." + random_base36(8))
tmp_p.symlink_to(path)
tmp_p.replace(p)
print(f"Saved path for {f} at {p} -> {path}")
default_login = 0
def nth(seq, n):
for i, si in enumerate(seq):
if i == n:
return si
raise IndexError
def get_config(login):
tea_config = platformdirs.user_config_path("tea") / "config.yml"
if tea_config.exists():
with open(tea_config) as f:
config = yaml_load(f)
return config["logins"][login]
fj_config = platformdirs.user_data_path("forgejo-cli") / "keys.json"
if fj_config.exists():
config = json.loads(fj_config.read_text())
aliases = config["aliases"]
hosts = config["hosts"]
while login in aliases:
login = aliases[login]
if isinstance(login, int):
host, data = nth(hosts.items(), login)
else:
host = login
data = hosts[login]
return {"url": f"https://{host}", "token": data["token"], "user": data["name"]}
raise SystemExit("Could not find configuration for tea or forgejo_cli")
def verbose():
import http.client as http_client
import logging
http_client.HTTPConnection.debuglevel = 1
# You must initialize logging, otherwise you'll not see debug output.
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
# verbose()
@click.command
@click.option("--login", type=str, default=None)
@click.option("--repo", type=str, default="junkdrawer")
@click.option("--path", type=str, default=None)
@click.option("--stdin-filename", "-f", type=str, default="stdin.txt")
@click.option("--dry-run/--no-dry-run", "-n", type=bool, default=False)
@click.argument("files", type=click.File("rb"), nargs=-1)
def main(
login, repo, path, stdin_filename, files: list[typing.BinaryIO], dry_run: bool
):
if not files:
files = [sys.stdin.buffer]
if login is None:
login = default_login
if isinstance(login, str) and login[0] in string.digits:
login = int(login)
config = get_config(login)
baseurl = config["url"]
token = config["token"]
user = config["user"]
headers = {
"Authorization": f"token {token}",
"Accept": "application/json",
}
first = True
if path is None:
for f in files:
f_path = pathlib.Path(f.name)
path = find_path_from_file(f_path)
if path is not None:
break
if path is None:
path = random_base36(8) # Simply ignore birthday paradox
for f in files:
f_path = pathlib.Path(f.name)
if not first:
print()
first = False
if f.name == "<stdin>":
filename = stdin_filename
else:
filename = pathlib.Path(f.name).name
filepath = f"{path}/{filename}"
qfilepath = urllib.parse.quote(filepath, "")
url = f"{baseurl}/api/v1/repos/{user}/{repo}/contents/{qfilepath}"
content: bytes = f.read()
sha = hashlib.new("sha1", content).hexdigest()
content_base64 = binascii.b2a_base64(content).strip().decode("ascii")
body = {"content": content_base64}
# ugugugug contents method is "POST" to create, "PUT" to change.
# Have to know if exists and the sha if so...
response = requests.get(url, headers=headers)
exists = response.status_code == 200
if exists:
body["sha"] = response.json()["sha"]
method = requests.put if exists else requests.post
if dry_run:
print(
f"Would upload to {url}, {'replacing existing' if exists else 'creating new'} file"
)
else:
response = method(url, headers=headers, json=body)
response.raise_for_status()
j = response.json()
html_url = j["content"]["html_url"]
raw_url = j["content"]["download_url"]
ref = j["commit"]["sha"][:12]
html_ref_url = f"{baseurl}/{user}/{repo}/src/commit/{ref}/{filepath}"
raw_ref_url = f"{baseurl}/{user}/{repo}/raw/commit/{ref}/{filepath}"
print(f"Uploaded {filepath} at {ref}")
print(f"HTML (HEAD): {html_url}")
print(f"HTML (ref): {html_ref_url}")
print(f"Raw (HEAD): {raw_url}")
print(f"Raw (ref): {raw_ref_url}")
print(f"Aether: [junk {filepath} [lang ...]]")
if not dry_run:
store_path_from_file(f_path, path)
if __name__ == "__main__":
main()