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()