mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
POST /api/repos/import/inspect reports what importing a clone in the media directory would do, without changing it: its origin (an SSH or git:// origin is converted to https://), branch, latest commit, commits not on the remote, whether it is shallow, and where it will be moved. It refuses special directories, Collections' directories, `.git` files/links, bare repos, and partial clones. It runs no git command which could run a program from the clone's config (the latest commit is read with cat-file, not log). POST /api/repos/import (confirmed, with a URL that is the clone's origin) moves the clone into the Repos directory (the old location is refreshed away), sanitizes its config, points origin at the URL, marks it as WROLPi's, and creates the Repo and its Download. The Repo shows the clone's commit and README at once; its first update makes it a mirror (local work is discarded, and the page warns of local commits). The Repos page has an Import button; the Import page inspects the chosen directory, then imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
178 lines
7 KiB
Python
178 lines
7 KiB
Python
from http import HTTPStatus
|
|
|
|
from sanic import Request, Blueprint, response
|
|
from sanic_ext.extensions.openapi import openapi
|
|
|
|
from wrolpi.api_utils import json_response
|
|
from wrolpi.common import wrol_mode_check, logger
|
|
from wrolpi.downloader import download_manager
|
|
from wrolpi.errors import ValidationError
|
|
from . import lib, schema
|
|
from .models import Repository
|
|
|
|
repos_bp = Blueprint('Repos', '/api/repos')
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
|
|
@repos_bp.get('/')
|
|
@openapi.definition(summary='List all git Repos')
|
|
async def get_repos(request: Request):
|
|
repos = lib.get_repositories(request.ctx.session)
|
|
return json_response(dict(repos=repos))
|
|
|
|
|
|
@repos_bp.post('/')
|
|
@openapi.definition(
|
|
summary='Add a git Repo; it will be cloned, then kept up to date',
|
|
body=schema.RepoCreateRequest,
|
|
validate=True,
|
|
)
|
|
@wrol_mode_check
|
|
async def post_repo(request: Request, body: schema.RepoCreateRequest):
|
|
session = request.ctx.session
|
|
repo = lib.create_repository(
|
|
session,
|
|
body.url,
|
|
tag_name=body.tag_name or None,
|
|
frequency=body.frequency or lib.DEFAULT_REPO_FREQUENCY,
|
|
mode=body.mode or 'full',
|
|
branch=body.branch or None,
|
|
name=body.name or None,
|
|
description=body.description or None,
|
|
submodules=body.submodules,
|
|
)
|
|
repo = Repository.find_by_id(session, repo.id, options=Repository.json_options())
|
|
return json_response(dict(repo=repo.__json__()), status=HTTPStatus.CREATED)
|
|
|
|
|
|
@repos_bp.post('/import/inspect')
|
|
@openapi.definition(
|
|
summary='What importing an existing git clone (in the media directory) as a Repo would do. Changes nothing.',
|
|
body=schema.RepoImportInspectRequest,
|
|
validate=True,
|
|
)
|
|
async def post_inspect_import(request: Request, body: schema.RepoImportInspectRequest):
|
|
info = lib.inspect_import(request.ctx.session, body.directory, body.name or None, body.tag_name or None)
|
|
return json_response(dict(inspection=info))
|
|
|
|
|
|
@repos_bp.post('/import')
|
|
@openapi.definition(
|
|
summary='Import an existing git clone as a Repo. It is moved into the Repos directory, and becomes a mirror of '
|
|
'its origin (local changes are discarded by its updates).',
|
|
body=schema.RepoImportRequest,
|
|
validate=True,
|
|
)
|
|
@wrol_mode_check
|
|
async def post_import(request: Request, body: schema.RepoImportRequest):
|
|
session = request.ctx.session
|
|
repo = await lib.import_repository(
|
|
session,
|
|
body.directory,
|
|
url=body.url or None,
|
|
confirm=body.confirm,
|
|
tag_name=body.tag_name or None,
|
|
frequency=body.frequency or lib.DEFAULT_REPO_FREQUENCY,
|
|
mode=body.mode or 'full',
|
|
branch=body.branch or None,
|
|
submodules=body.submodules,
|
|
name=body.name or None,
|
|
description=body.description or None,
|
|
)
|
|
repo = Repository.find_by_id(session, repo.id, options=Repository.json_options())
|
|
return json_response(dict(repo=repo.__json__()), status=HTTPStatus.CREATED)
|
|
|
|
|
|
@repos_bp.post('/search')
|
|
@openapi.definition(
|
|
summary='Search git Repos by their name and README, and/or by Tag',
|
|
body=schema.RepoSearchRequest,
|
|
validate=True,
|
|
)
|
|
async def post_search_repos(request: Request, body: schema.RepoSearchRequest):
|
|
limit = min(max(body.limit, 1), 100)
|
|
repos, total = lib.search_repos(request.ctx.session, body.search_str, body.tag_names, limit=limit,
|
|
offset=max(body.offset, 0))
|
|
return json_response(dict(repos=repos, totals=dict(repos=total)))
|
|
|
|
|
|
@repos_bp.get('/<repo_id:int>')
|
|
@openapi.definition(summary='Get a git Repo')
|
|
async def get_repo(request: Request, repo_id: int):
|
|
repo = Repository.find_by_id(request.ctx.session, repo_id, options=Repository.json_options())
|
|
return json_response(dict(repo=repo.__json__()))
|
|
|
|
|
|
@repos_bp.put('/<repo_id:int>')
|
|
@openapi.definition(
|
|
summary="Change a git Repo's settings",
|
|
body=schema.RepoUpdateRequest,
|
|
validate=True,
|
|
)
|
|
@wrol_mode_check
|
|
async def put_repo(request: Request, repo_id: int, body: schema.RepoUpdateRequest):
|
|
session = request.ctx.session
|
|
lib.update_repository(session, repo_id, description=body.description, frequency=body.frequency,
|
|
mode=body.mode, branch=body.branch, submodules=body.submodules)
|
|
repo = Repository.find_by_id(session, repo_id, options=Repository.json_options())
|
|
return json_response(dict(repo=repo.__json__()))
|
|
|
|
|
|
@repos_bp.delete('/<repo_id:int>')
|
|
@openapi.definition(summary='Delete a git Repo. Its files are kept unless `delete_files=true`.')
|
|
@wrol_mode_check
|
|
async def delete_repo(request: Request, repo_id: int):
|
|
delete_files = request.args.get('delete_files', '').lower() in ('1', 'true', 'yes')
|
|
await lib.delete_repository(request.ctx.session, repo_id, delete_files=delete_files)
|
|
return response.empty()
|
|
|
|
|
|
@repos_bp.post('/<repo_id:int>/update')
|
|
@openapi.definition(summary='Update a git Repo now')
|
|
@wrol_mode_check
|
|
async def post_update_repo(request: Request, repo_id: int):
|
|
session = request.ctx.session
|
|
repo = Repository.find_by_id(session, repo_id, options=Repository.json_options())
|
|
if not (download := repo.download):
|
|
raise ValidationError('Repo has no download')
|
|
download_manager.restart_download(session, download.id)
|
|
return response.empty(HTTPStatus.NO_CONTENT)
|
|
|
|
|
|
@repos_bp.get('/<repo_id:int>/tree')
|
|
@openapi.definition(summary="List a directory of a git Repo's files")
|
|
@openapi.parameter('path', str, 'query', description='Directory in the repo, relative to its root', required=False)
|
|
async def get_repo_tree(request: Request, repo_id: int):
|
|
repo = Repository.find_by_id(request.ctx.session, repo_id)
|
|
tree = lib.list_repo_tree(repo, request.args.get('path', ''))
|
|
return json_response(tree)
|
|
|
|
|
|
@repos_bp.get('/<repo_id:int>/log')
|
|
@openapi.definition(summary="The commits of a git Repo's checked out branch, newest first")
|
|
@openapi.parameter('limit', int, 'query', description='At most 100', required=False)
|
|
@openapi.parameter('offset', int, 'query', required=False)
|
|
async def get_repo_log(request: Request, repo_id: int):
|
|
repo = Repository.find_by_id(request.ctx.session, repo_id)
|
|
try:
|
|
limit = int(request.args.get('limit', 50))
|
|
offset = int(request.args.get('offset', 0))
|
|
except ValueError:
|
|
raise ValidationError('limit and offset must be numbers')
|
|
log = await lib.get_repo_log(repo, limit=limit, offset=offset)
|
|
return json_response(log)
|
|
|
|
|
|
@repos_bp.get('/<repo_id:int>/archive.zip')
|
|
@openapi.definition(summary="Download a ZIP of a git Repo's checked out files")
|
|
async def get_repo_archive(request: Request, repo_id: int):
|
|
repo = Repository.find_by_id(request.ctx.session, repo_id)
|
|
# Read everything from the database before the response starts; the request's session closes then.
|
|
directory = lib.get_repo_root(repo)
|
|
name = lib.repo_archive_name(repo)
|
|
response = await request.respond(content_type='application/zip', headers={
|
|
'Content-Disposition': f'attachment; filename="{name}.zip"',
|
|
})
|
|
await lib.stream_repo_archive(directory, name, response.send)
|
|
await response.eof()
|