発散解像度 -divergence resolution-
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
This commit is contained in:
parent
1ff613dee6
commit
01bb65f8da
457 changed files with 929 additions and 602 deletions
0
api/funkwhale_api/cli/__init__.py
Normal file
0
api/funkwhale_api/cli/__init__.py
Normal file
66
api/funkwhale_api/cli/base.py
Normal file
66
api/funkwhale_api/cli/base.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import functools
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
||||
def confirm_action(f, id_var, message_template="Do you want to proceed?"):
|
||||
@functools.wraps(f)
|
||||
def action(*args, **kwargs):
|
||||
if id_var:
|
||||
id_value = kwargs[id_var]
|
||||
message = message_template.format(len(id_value))
|
||||
else:
|
||||
message = message_template
|
||||
if not kwargs.pop("no_input", False) and not click.confirm(message, abort=True):
|
||||
return
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def delete_command(
|
||||
group,
|
||||
id_var="id",
|
||||
name="rm",
|
||||
message_template="Do you want to delete {} objects? This action is irreversible.",
|
||||
):
|
||||
"""
|
||||
Wrap a command to ensure it asks for confirmation before deletion, unless the --no-input
|
||||
flag is provided
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
decorated = click.option("--no-input", is_flag=True)(f)
|
||||
decorated = confirm_action(
|
||||
decorated, id_var=id_var, message_template=message_template
|
||||
)
|
||||
return group.command(name)(decorated)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def update_command(
|
||||
group,
|
||||
id_var="id",
|
||||
name="set",
|
||||
message_template="Do you want to update {} objects? This action may have irreversible consequnces.",
|
||||
):
|
||||
"""
|
||||
Wrap a command to ensure it asks for confirmation before deletion, unless the --no-input
|
||||
flag is provided
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
decorated = click.option("--no-input", is_flag=True)(f)
|
||||
decorated = confirm_action(
|
||||
decorated, id_var=id_var, message_template=message_template
|
||||
)
|
||||
return group.command(name)(decorated)
|
||||
|
||||
return decorator
|
||||
51
api/funkwhale_api/cli/library.py
Normal file
51
api/funkwhale_api/cli/library.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import click
|
||||
|
||||
from funkwhale_api.music import tasks
|
||||
|
||||
from . import base
|
||||
|
||||
|
||||
def handler_add_tags_from_tracks(
|
||||
artists=False,
|
||||
albums=False,
|
||||
):
|
||||
result = None
|
||||
if artists:
|
||||
result = tasks.artists_set_tags_from_tracks()
|
||||
elif albums:
|
||||
result = tasks.albums_set_tags_from_tracks()
|
||||
else:
|
||||
raise click.BadOptionUsage("You must specify artists or albums")
|
||||
|
||||
if result is None:
|
||||
click.echo(" No relevant tags found")
|
||||
else:
|
||||
click.echo(f" Relevant tags added to {len(result)} objects")
|
||||
|
||||
|
||||
@base.cli.group()
|
||||
def albums():
|
||||
"""Manage albums"""
|
||||
pass
|
||||
|
||||
|
||||
@base.cli.group()
|
||||
def artists():
|
||||
"""Manage artists"""
|
||||
pass
|
||||
|
||||
|
||||
@albums.command(name="add-tags-from-tracks")
|
||||
def albums_add_tags_from_tracks():
|
||||
"""
|
||||
Associate tags to album with no genre tags, assuming identical tags are found on the album tracks
|
||||
"""
|
||||
handler_add_tags_from_tracks(albums=True)
|
||||
|
||||
|
||||
@artists.command(name="add-tags-from-tracks")
|
||||
def artists_add_tags_from_tracks():
|
||||
"""
|
||||
Associate tags to artists with no genre tags, assuming identical tags are found on the artist tracks
|
||||
"""
|
||||
handler_add_tags_from_tracks(artists=True)
|
||||
22
api/funkwhale_api/cli/main.py
Normal file
22
api/funkwhale_api/cli/main.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import sys
|
||||
|
||||
import click
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from . import library # noqa
|
||||
from . import media # noqa
|
||||
from . import plugins # noqa
|
||||
from . import users # noqa
|
||||
from . import base
|
||||
|
||||
|
||||
def invoke():
|
||||
try:
|
||||
return base.cli()
|
||||
except ValidationError as e:
|
||||
click.secho("Invalid data:", fg="red")
|
||||
for field, errors in e.detail.items():
|
||||
click.secho(f" {field}:", fg="red")
|
||||
for error in errors:
|
||||
click.secho(f" - {error}", fg="red")
|
||||
sys.exit(1)
|
||||
61
api/funkwhale_api/cli/media.py
Normal file
61
api/funkwhale_api/cli/media.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import click
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.files.storage import default_storage
|
||||
from versatileimagefield import settings as vif_settings
|
||||
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
||||
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.common.models import Attachment
|
||||
|
||||
from . import base
|
||||
|
||||
|
||||
@base.cli.group()
|
||||
def media():
|
||||
"""Manage media files (avatars, covers, attachments…)"""
|
||||
pass
|
||||
|
||||
|
||||
@media.command("generate-thumbnails")
|
||||
@click.option("-d", "--delete", is_flag=True)
|
||||
def generate_thumbnails(delete):
|
||||
"""
|
||||
Generate thumbnails for all images (avatars, covers, etc.).
|
||||
|
||||
This can take a long time and generate a lot of I/O depending of the size
|
||||
of your library.
|
||||
"""
|
||||
click.echo("Deleting existing thumbnails…")
|
||||
if delete:
|
||||
try:
|
||||
# FileSystemStorage doesn't support deleting a non-empty directory
|
||||
# so we reimplemented a method to do so
|
||||
default_storage.force_delete("__sized__")
|
||||
except AttributeError:
|
||||
# backends doesn't support directory deletion
|
||||
pass
|
||||
MODELS = [
|
||||
(Attachment, "file", "attachment_square"),
|
||||
]
|
||||
for model, attribute, key_set in MODELS:
|
||||
click.echo(f"Generating thumbnails for {model._meta.label}.{attribute}…")
|
||||
qs = model.objects.exclude(**{f"{attribute}__isnull": True})
|
||||
qs = qs.exclude(**{attribute: ""})
|
||||
cache_key = "*{}{}*".format(
|
||||
settings.MEDIA_URL, vif_settings.VERSATILEIMAGEFIELD_SIZED_DIRNAME
|
||||
)
|
||||
entries = cache.keys(cache_key)
|
||||
if entries:
|
||||
click.echo(f" Clearing {len(entries)} cache entries: {cache_key}…")
|
||||
for keys in common_utils.batch(iter(entries)):
|
||||
cache.delete_many(keys)
|
||||
warmer = VersatileImageFieldWarmer(
|
||||
instance_or_queryset=qs,
|
||||
rendition_key_set=key_set,
|
||||
image_attr=attribute,
|
||||
verbose=True,
|
||||
)
|
||||
click.echo(" Creating images")
|
||||
num_created, failed_to_create = warmer.warm()
|
||||
click.echo(f" {num_created} created, {len(failed_to_create)} in error")
|
||||
34
api/funkwhale_api/cli/plugins.py
Normal file
34
api/funkwhale_api/cli/plugins.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import click
|
||||
from django.conf import settings
|
||||
|
||||
from . import base
|
||||
|
||||
|
||||
@base.cli.group()
|
||||
def plugins():
|
||||
"""Manage plugins"""
|
||||
pass
|
||||
|
||||
|
||||
@plugins.command("install")
|
||||
@click.argument("plugin", nargs=-1)
|
||||
def install(plugin):
|
||||
"""
|
||||
Install a plugin from a given URL (zip, pip or git are supported)
|
||||
"""
|
||||
if not plugin:
|
||||
return click.echo("No plugin provided")
|
||||
|
||||
click.echo("Installing plugins…")
|
||||
pip_install(list(plugin), settings.FUNQUAIL_PLUGINS_PATH)
|
||||
|
||||
|
||||
def pip_install(deps, target):
|
||||
if not deps:
|
||||
return
|
||||
pip_path = os.path.join(os.path.dirname(sys.executable), "pip")
|
||||
subprocess.check_call([pip_path, "install", "-t", target] + deps)
|
||||
235
api/funkwhale_api/cli/users.py
Normal file
235
api/funkwhale_api/cli/users.py
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import click
|
||||
from django.db import transaction
|
||||
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.users import models, serializers, tasks
|
||||
|
||||
from . import base, utils
|
||||
|
||||
|
||||
class FakeRequest:
|
||||
def __init__(self, session={}):
|
||||
self.session = session
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def handler_create_user(
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
is_superuser=False,
|
||||
is_staff=False,
|
||||
permissions=[],
|
||||
upload_quota=None,
|
||||
):
|
||||
serializer = serializers.RS(
|
||||
data={
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password1": password,
|
||||
"password2": password,
|
||||
}
|
||||
)
|
||||
utils.logger.debug("Validating user data…")
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Override e-mail validation, we assume accounts created from CLI have a valid e-mail
|
||||
request = FakeRequest(session={"account_verified_email": email})
|
||||
utils.logger.debug("Creating user…")
|
||||
user = serializer.save(request=request)
|
||||
utils.logger.debug("Setting permissions and other attributes…")
|
||||
user.is_staff = is_staff or is_superuser # Always set staff if superuser is set
|
||||
user.upload_quota = upload_quota
|
||||
user.is_superuser = is_superuser
|
||||
for permission in permissions:
|
||||
if permission in models.PERMISSIONS:
|
||||
utils.logger.debug("Setting %s permission to True", permission)
|
||||
setattr(user, f"permission_{permission}", True)
|
||||
else:
|
||||
utils.logger.warn("Unknown permission %s", permission)
|
||||
utils.logger.debug("Creating actor…")
|
||||
user.actor = models.create_actor(user)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def handler_delete_user(usernames, soft=True):
|
||||
for username in usernames:
|
||||
click.echo(f"Deleting {username}…")
|
||||
actor = None
|
||||
user = None
|
||||
try:
|
||||
user = models.User.objects.get(username=username)
|
||||
except models.User.DoesNotExist:
|
||||
try:
|
||||
actor = federation_models.Actor.objects.local().get(
|
||||
preferred_username=username
|
||||
)
|
||||
except federation_models.Actor.DoesNotExist:
|
||||
click.echo(" Not found, skipping")
|
||||
continue
|
||||
|
||||
actor = actor or user.actor
|
||||
if user:
|
||||
tasks.delete_account(user_id=user.pk)
|
||||
if not soft:
|
||||
click.echo(" Hard delete, removing actor")
|
||||
actor.delete()
|
||||
click.echo(" Done")
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def handler_update_user(usernames, kwargs):
|
||||
users = models.User.objects.filter(username__in=usernames)
|
||||
total = users.count()
|
||||
if not total:
|
||||
click.echo("No matching users")
|
||||
return
|
||||
|
||||
final_kwargs = {}
|
||||
supported_fields = [
|
||||
"is_active",
|
||||
"permission_moderation",
|
||||
"permission_library",
|
||||
"permission_settings",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"upload_quota",
|
||||
"password",
|
||||
]
|
||||
for field in supported_fields:
|
||||
try:
|
||||
value = kwargs[field]
|
||||
except KeyError:
|
||||
continue
|
||||
final_kwargs[field] = value
|
||||
|
||||
click.echo(
|
||||
"Updating {} on {} matching users…".format(
|
||||
", ".join(final_kwargs.keys()), total
|
||||
)
|
||||
)
|
||||
if "password" in final_kwargs:
|
||||
new_password = final_kwargs.pop("password")
|
||||
for user in users:
|
||||
user.set_password(new_password)
|
||||
models.User.objects.bulk_update(users, ["password"])
|
||||
if final_kwargs:
|
||||
users.update(**final_kwargs)
|
||||
click.echo("Done!")
|
||||
|
||||
|
||||
@base.cli.group()
|
||||
def users():
|
||||
"""Manage users"""
|
||||
pass
|
||||
|
||||
|
||||
@users.command()
|
||||
@click.option("--username", "-u", prompt=True, required=True)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--password",
|
||||
prompt="Password (leave empty to have a random one generated)",
|
||||
hide_input=True,
|
||||
envvar="FUNQUAIL_CLI_USER_PASSWORD",
|
||||
default="",
|
||||
help="If empty, a random password will be generated and displayed in console output",
|
||||
)
|
||||
@click.option(
|
||||
"-e",
|
||||
"--email",
|
||||
prompt=True,
|
||||
help="Email address to associate with the account",
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"-q",
|
||||
"--upload-quota",
|
||||
help="Upload quota (leave empty to use default pod quota)",
|
||||
required=False,
|
||||
default=None,
|
||||
type=click.INT,
|
||||
)
|
||||
@click.option(
|
||||
"--superuser/--no-superuser",
|
||||
default=False,
|
||||
)
|
||||
@click.option(
|
||||
"--staff/--no-staff",
|
||||
default=False,
|
||||
)
|
||||
@click.option(
|
||||
"--permission",
|
||||
multiple=True,
|
||||
)
|
||||
def create(username, password, email, superuser, staff, permission, upload_quota):
|
||||
"""Create a new user"""
|
||||
generated_password = None
|
||||
if password == "":
|
||||
generated_password = models.User.objects.make_random_password()
|
||||
user = handler_create_user(
|
||||
username=username,
|
||||
password=password or generated_password,
|
||||
email=email,
|
||||
is_superuser=superuser,
|
||||
is_staff=staff,
|
||||
permissions=permission,
|
||||
upload_quota=upload_quota,
|
||||
)
|
||||
click.echo(f"User {user.username} created!")
|
||||
if generated_password:
|
||||
click.echo(f" Generated password: {generated_password}")
|
||||
|
||||
|
||||
@base.delete_command(group=users, id_var="username")
|
||||
@click.argument("username", nargs=-1)
|
||||
@click.option(
|
||||
"--hard/--no-hard",
|
||||
default=False,
|
||||
help="Purge all user-related info (allow recreating a user with the same username)",
|
||||
)
|
||||
def delete(username, hard):
|
||||
"""Delete given users"""
|
||||
handler_delete_user(usernames=username, soft=not hard)
|
||||
|
||||
|
||||
@base.update_command(group=users, id_var="username")
|
||||
@click.argument("username", nargs=-1)
|
||||
@click.option(
|
||||
"--active/--inactive",
|
||||
help="Mark as active or inactive (inactive users cannot login or use the service)",
|
||||
default=None,
|
||||
)
|
||||
@click.option("--superuser/--no-superuser", default=None)
|
||||
@click.option("--staff/--no-staff", default=None)
|
||||
@click.option("--permission-library/--no-permission-library", default=None)
|
||||
@click.option("--permission-moderation/--no-permission-moderation", default=None)
|
||||
@click.option("--permission-settings/--no-permission-settings", default=None)
|
||||
@click.option("--password", default=None, envvar="FUNQUAIL_CLI_USER_UPDATE_PASSWORD")
|
||||
@click.option(
|
||||
"-q",
|
||||
"--upload-quota",
|
||||
type=click.INT,
|
||||
)
|
||||
def update(username, **kwargs):
|
||||
"""Update attributes for given users"""
|
||||
field_mapping = {
|
||||
"active": "is_active",
|
||||
"superuser": "is_superuser",
|
||||
"staff": "is_staff",
|
||||
}
|
||||
final_kwargs = {}
|
||||
for cli_field, value in kwargs.items():
|
||||
if value is None:
|
||||
continue
|
||||
model_field = (
|
||||
field_mapping[cli_field] if cli_field in field_mapping else cli_field
|
||||
)
|
||||
final_kwargs[model_field] = value
|
||||
|
||||
if not final_kwargs:
|
||||
raise click.BadArgumentUsage("You need to update at least one attribute")
|
||||
|
||||
handler_update_user(usernames=username, kwargs=final_kwargs)
|
||||
3
api/funkwhale_api/cli/utils.py
Normal file
3
api/funkwhale_api/cli/utils.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import logging
|
||||
|
||||
logger = logging.getLogger("funkwhale_api.cli")
|
||||
Loading…
Add table
Add a link
Reference in a new issue