154 lines
4.5 KiB
Python
Executable file
154 lines
4.5 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# hikari_uploader
|
|
# Copyright (C) 2024 odrling
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import hashlib
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
import pyperclip
|
|
import requests
|
|
import typer
|
|
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
|
from rich.console import Console
|
|
from rich.progress import (BarColumn, DownloadColumn, Progress, TextColumn,
|
|
TimeRemainingColumn, TransferSpeedColumn)
|
|
|
|
HIKARI_BASE = "https://hikari.butaishoujo.moe"
|
|
HIKARI_URL = f"{HIKARI_BASE}/upload"
|
|
|
|
console = Console()
|
|
|
|
|
|
@dataclass
|
|
class UploadResponse:
|
|
status: Literal["exists"] | Literal["uploaded"]
|
|
url: str
|
|
|
|
|
|
def check_hash(filename: str, file: Path):
|
|
h = hashlib.sha256()
|
|
with file.open('br') as f:
|
|
while buf := f.read(1024**2):
|
|
h.update(buf)
|
|
|
|
h.hexdigest()[:8]
|
|
url = f"{HIKARI_URL}/{h.hexdigest()[:8]}"
|
|
|
|
with file.open('br') as f:
|
|
files = {'file': (filename, f.read(2048))}
|
|
|
|
with requests.post(url, files=files, stream=True) as req:
|
|
if req.status_code == 404:
|
|
return
|
|
|
|
req.raise_for_status()
|
|
return UploadResponse(**req.json())
|
|
|
|
|
|
def output(copy: bool, resp: UploadResponse):
|
|
if copy:
|
|
pyperclip.copy(resp.url)
|
|
action = "Copied"
|
|
elif resp.status == "exists":
|
|
action = "Found"
|
|
elif resp.status == "uploaded":
|
|
action = "Uploaded"
|
|
else:
|
|
action = ""
|
|
|
|
console.print(action, resp.url, style="bold green")
|
|
|
|
|
|
@dataclass
|
|
class ProgressMonitor:
|
|
size: int
|
|
filename: str
|
|
|
|
def __post_init__(self):
|
|
self.progress = Progress(
|
|
TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
|
|
BarColumn(bar_width=None),
|
|
"[progress.percentage]{task.percentage:>3.1f}%",
|
|
"•",
|
|
DownloadColumn(),
|
|
"•",
|
|
TransferSpeedColumn(),
|
|
"•",
|
|
TimeRemainingColumn(),
|
|
)
|
|
self.task = self.progress.add_task("[green]Uploading",
|
|
filename=self.filename,
|
|
total=self.size)
|
|
self.progress.start()
|
|
|
|
def __call__(self, monitor: MultipartEncoderMonitor):
|
|
progress = monitor.bytes_read
|
|
self.progress.update(self.task, completed=progress)
|
|
|
|
|
|
def upload(file: Path,
|
|
obstruct: bool = typer.Option(
|
|
False, "--obstruct", "-o",
|
|
help="Obstruct filename (by hashing)"
|
|
),
|
|
filename: str = typer.Option(
|
|
None, "--name", "-n",
|
|
help="Rename the uploaded file."
|
|
),
|
|
copy: bool = typer.Option(
|
|
False, "--copy", "-c",
|
|
help="Copy file URL to clipboard."
|
|
),
|
|
hash: bool = typer.Option(
|
|
True, "--hash/--no-hash", "-h/-H",
|
|
help="Hash file before uploading to avoid having to send the whole file twice."
|
|
)):
|
|
|
|
if copy:
|
|
pyperclip.copy("")
|
|
|
|
if obstruct:
|
|
filename = f"{hashlib.sha256(file.name.encode()).hexdigest()}{file.suffix}"
|
|
elif filename is None:
|
|
filename = file.name
|
|
|
|
if hash and (resp := check_hash(filename, file)):
|
|
return output(copy, resp)
|
|
|
|
with file.open('br') as f:
|
|
files = MultipartEncoder(
|
|
fields={'file': (filename, f)}
|
|
)
|
|
progress_monitor = ProgressMonitor(files.len, filename)
|
|
|
|
monitored = MultipartEncoderMonitor(files, progress_monitor)
|
|
headers = {'Content-Type': monitored.content_type}
|
|
|
|
with requests.post(HIKARI_URL,
|
|
data=monitored, # type: ignore
|
|
headers=headers) as req:
|
|
progress_monitor.progress.stop()
|
|
|
|
req.raise_for_status()
|
|
data = UploadResponse(**req.json())
|
|
output(copy, data)
|
|
|
|
|
|
def main():
|
|
typer.run(upload)
|