mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2025-05-14 07:04:27 -04:00
add defaults and system plugins
This commit is contained in:
parent
0c878eb754
commit
d0e3c9502e
16 changed files with 1131 additions and 146 deletions
|
@ -59,12 +59,17 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
|
'solo',
|
||||||
|
|
||||||
|
|
||||||
'core',
|
'core',
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
'plugins.replaywebpage',
|
|
||||||
'plugins.gallerydl',
|
'plugins.defaults',
|
||||||
|
'plugins.system',
|
||||||
|
# 'plugins.replaywebpage',
|
||||||
|
# 'plugins.gallerydl',
|
||||||
# 'plugins.browsertrix',
|
# 'plugins.browsertrix',
|
||||||
# 'plugins.playwright',
|
# 'plugins.playwright',
|
||||||
# ...
|
# ...
|
||||||
|
@ -87,8 +92,9 @@ STATICFILES_DIRS = [
|
||||||
str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'static'),
|
str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'static'),
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
str(Path(PACKAGE_DIR) / 'plugins/replaywebpage/static'),
|
# str(Path(PACKAGE_DIR) / 'plugins/defaults/static'),
|
||||||
str(Path(PACKAGE_DIR) / 'plugins/gallerydl/static'),
|
# str(Path(PACKAGE_DIR) / 'plugins/replaywebpage/static'),
|
||||||
|
# str(Path(PACKAGE_DIR) / 'plugins/gallerydl/static'),
|
||||||
# str(Path(PACKAGE_DIR) / 'plugins/browsertrix/static'),
|
# str(Path(PACKAGE_DIR) / 'plugins/browsertrix/static'),
|
||||||
# str(Path(PACKAGE_DIR) / 'plugins/playwright/static'),
|
# str(Path(PACKAGE_DIR) / 'plugins/playwright/static'),
|
||||||
# ...
|
# ...
|
||||||
|
@ -107,8 +113,10 @@ TEMPLATE_DIRS = [
|
||||||
str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME),
|
str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME),
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
str(Path(PACKAGE_DIR) / 'plugins/replaywebpage/templates'),
|
# added by plugins.<PluginName>.apps.<AppName>.ready -> .settings.register_plugin_settings
|
||||||
str(Path(PACKAGE_DIR) / 'plugins/gallerydl/templates'),
|
# str(Path(PACKAGE_DIR) / 'plugins/defaults/templates'),
|
||||||
|
# str(Path(PACKAGE_DIR) / 'plugins/replaywebpage/templates'),
|
||||||
|
# str(Path(PACKAGE_DIR) / 'plugins/gallerydl/templates'),
|
||||||
# str(Path(PACKAGE_DIR) / 'plugins/browsertrix/templates'),
|
# str(Path(PACKAGE_DIR) / 'plugins/browsertrix/templates'),
|
||||||
# str(Path(PACKAGE_DIR) / 'plugins/playwright/templates'),
|
# str(Path(PACKAGE_DIR) / 'plugins/playwright/templates'),
|
||||||
# ...
|
# ...
|
||||||
|
|
21
archivebox/plugins/defaults/admin.py
Normal file
21
archivebox/plugins/defaults/admin.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from solo.admin import SingletonModelAdmin
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
ArchiveBoxDefaultDependency,
|
||||||
|
ArchiveBoxDefaultExtractor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyAdmin(SingletonModelAdmin):
|
||||||
|
readonly_fields = ('REQUIRED', 'ENABLED', 'BINARY', 'ARGS', 'bin_path', 'bin_version', 'is_valid', 'is_enabled')
|
||||||
|
|
||||||
|
class ExtractorAdmin(SingletonModelAdmin):
|
||||||
|
# readonly_fields = ('REQUIRED', 'ENABLED', 'BINARY', 'ARGS', 'bin_path', 'bin_version', 'is_valid', 'is_enabled')
|
||||||
|
pass
|
||||||
|
|
||||||
|
print('DefaultsPluginConfig.admin')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(ArchiveBoxDefaultDependency, DependencyAdmin)
|
||||||
|
admin.site.register(ArchiveBoxDefaultExtractor, ExtractorAdmin)
|
22
archivebox/plugins/defaults/apps.py
Normal file
22
archivebox/plugins/defaults/apps.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
__package__ = 'archivebox.plugins.defaults'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultsPluginConfig(AppConfig):
|
||||||
|
label = "ArchiveBox Defaults"
|
||||||
|
name = "defaults"
|
||||||
|
|
||||||
|
default_auto_field = "django.db.models.AutoField"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
print('plugins.defaults.apps.DefaultsPluginConfig.ready')
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .settings import register_plugin_settings
|
||||||
|
|
||||||
|
register_plugin_settings(settings, name=self.name)
|
||||||
|
|
39
archivebox/plugins/defaults/migrations/0001_initial.py
Normal file
39
archivebox/plugins/defaults/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 3.1.14 on 2024-01-24 08:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ArchiveBoxDefaultDependency',
|
||||||
|
fields=[
|
||||||
|
('ENABLED', models.BooleanField(default=True, editable=False)),
|
||||||
|
('BINARY', models.CharField(default='/bin/false', max_length=255)),
|
||||||
|
('ARGS', models.CharField(default='', max_length=255)),
|
||||||
|
('id', models.AutoField(default=1, primary_key=True, serialize=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ArchiveBoxDefaultExtractor',
|
||||||
|
fields=[
|
||||||
|
('ENABLED', models.BooleanField(default=True)),
|
||||||
|
('CMD', models.CharField(default=['{DEPENDENCY.BINARY}', '{ARGS}', '{url}'], max_length=255)),
|
||||||
|
('ARGS', models.CharField(default=['--timeout={TIMEOUT}'], max_length=255)),
|
||||||
|
('TIMEOUT', models.CharField(default='{TIMEOUT}', max_length=255)),
|
||||||
|
('id', models.AutoField(default=1, primary_key=True, serialize=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
archivebox/plugins/defaults/migrations/__init__.py
Normal file
0
archivebox/plugins/defaults/migrations/__init__.py
Normal file
361
archivebox/plugins/defaults/models.py
Normal file
361
archivebox/plugins/defaults/models.py
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
# __package__ = 'archivebox.plugins.defaults'
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.db import models, transaction
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
from solo.models import SingletonModel
|
||||||
|
|
||||||
|
ConfigDict = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def bin_path(binary: str) -> str | None:
|
||||||
|
return shutil.which(str(Path(binary).expanduser())) or shutil.which(str(binary)) or binary
|
||||||
|
|
||||||
|
def bin_version(bin_path: str, cmd: str | None=None) -> str | None:
|
||||||
|
return '0.0.0'
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveBoxBaseDependency(SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(default=singleton_instance_id, primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'DEFAULT'
|
||||||
|
LABEL = "Default"
|
||||||
|
REQUIRED = False
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = []
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = []
|
||||||
|
APT_DEPENDENCIES = []
|
||||||
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_DEPENDENCIES = []
|
||||||
|
NPM_DEPENDENCIES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = '/bin/false'
|
||||||
|
DEFAULT_START_CMD = '/bin/false'
|
||||||
|
DEFAULT_PID_FILE = 'logs/{NAME}_WORKER.pid'
|
||||||
|
DEFAULT_STOP_CMD = 'kill "$(<{PID_FILE})"'
|
||||||
|
DEFAULT_VERSION_COMMAND = '{CMD} --version'
|
||||||
|
DEFAULT_ARGS = ''
|
||||||
|
|
||||||
|
VERSION_CMD = '{BINARY} --version'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=False)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS)
|
||||||
|
|
||||||
|
# START_CMD = models.CharField(max_length=255, default=DEFAULT_START_CMD)
|
||||||
|
# WORKERS = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
app_label = 'defaults'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{self.LABEL} Dependency Configuration"
|
||||||
|
|
||||||
|
def __json__(self):
|
||||||
|
return {
|
||||||
|
'type': 'ArchiveBoxDependency',
|
||||||
|
'__class__': self.__class__.__name__,
|
||||||
|
'NAME': self.NAME,
|
||||||
|
'LABEL': self.LABEL,
|
||||||
|
'ENABLED': self.ENABLED,
|
||||||
|
'BINARY': self.BINARY,
|
||||||
|
'ARGS': self.ARGS,
|
||||||
|
# 'START_CMD': self.START_CMD,
|
||||||
|
# 'WORKERS': self.WORKERS,
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_path(self):
|
||||||
|
return bin_path(self.BINARY or self.DEFAULT_BINARY)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_version(self):
|
||||||
|
return bin_version(self.bin_path, cmd=self.VERSION_CMD)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_valid(self):
|
||||||
|
return bool(self.bin_path and self.bin_version)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_enabled(self):
|
||||||
|
return bool(self.ENABLED and self.is_valid)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def pretty_version(self):
|
||||||
|
if self.enabled:
|
||||||
|
if self.is_valid:
|
||||||
|
color, symbol, note, version = 'green', '√', 'valid', ''
|
||||||
|
|
||||||
|
parsed_version_num = re.search(r'[\d\.]+', self.bin_version)
|
||||||
|
if parsed_version_num:
|
||||||
|
version = f'v{parsed_version_num[0]}'
|
||||||
|
|
||||||
|
if not self.bin_version:
|
||||||
|
color, symbol, note, version = 'red', 'X', 'invalid', '?'
|
||||||
|
else:
|
||||||
|
color, symbol, note, version = 'lightyellow', '-', 'disabled', '-'
|
||||||
|
|
||||||
|
path = pretty_path(self.bin_path)
|
||||||
|
|
||||||
|
return ' '.join((
|
||||||
|
ANSI[color],
|
||||||
|
symbol,
|
||||||
|
ANSI['reset'],
|
||||||
|
name.ljust(21),
|
||||||
|
version.ljust(14),
|
||||||
|
ANSI[color],
|
||||||
|
note.ljust(8),
|
||||||
|
ANSI['reset'],
|
||||||
|
path.ljust(76),
|
||||||
|
))
|
||||||
|
|
||||||
|
# @helper
|
||||||
|
def install_parents(self, config):
|
||||||
|
return {
|
||||||
|
parent_dependency.NAME: parent_dependency.get_solo().install_self()
|
||||||
|
for parent_dependency in self.PARENT_DEPENDENCIES
|
||||||
|
}
|
||||||
|
|
||||||
|
# @helper
|
||||||
|
def install_self(self, config):
|
||||||
|
assert all(self.install_parents().values())
|
||||||
|
|
||||||
|
BashEnvironmentDependency.get_solo().install_pkgs(self.BIN_DEPENDENCIES)
|
||||||
|
AptEnvironmentDependency.get_solo().install_pkgs(self.APT_DEPENDENCIES)
|
||||||
|
BrewEnvironmentDependency.get_solo().install_pkgs(self.BREW_DEPENDENCIES)
|
||||||
|
PipEnvironmentDependency.get_solo().install_pkgs(self.PIP_DEPENDENCIES)
|
||||||
|
NPMEnvironmentDependency.get_solo().install_pkgs(self.NPM_DEPENDENCIES)
|
||||||
|
|
||||||
|
assert self.is_valid
|
||||||
|
return self.bin_version
|
||||||
|
|
||||||
|
# @task
|
||||||
|
def run(args, pwd, timeout):
|
||||||
|
errors = None
|
||||||
|
timer = TimedProgress(timeout, prefix=' ')
|
||||||
|
try:
|
||||||
|
proc = run(cmd=[self.bin_path, *args], pwd=pwd, timeout=timeout)
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
errors = err
|
||||||
|
finally:
|
||||||
|
timer.end()
|
||||||
|
|
||||||
|
return proc, timer, errors
|
||||||
|
|
||||||
|
class ArchiveBoxDefaultDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(default=singleton_instance_id, primary_key=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'defaults'
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveBoxBaseExtractor(SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(default=singleton_instance_id, primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'DEFAULT'
|
||||||
|
LABEL = 'Default'
|
||||||
|
|
||||||
|
DEFAULT_DEPENDENCY = ArchiveBoxDefaultDependency
|
||||||
|
DEPENDENCY = DEFAULT_DEPENDENCY
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ENABLED = True
|
||||||
|
DEFAULT_CMD = ['{DEPENDENCY.BINARY}', '{ARGS}', '{url}']
|
||||||
|
DEFAULT_ARGS = ['--timeout={TIMEOUT}']
|
||||||
|
DEFAULT_TIMEOUT = '{TIMEOUT}'
|
||||||
|
# DEFAULT_USER_AGENT = '{USER_AGENT}'
|
||||||
|
# DEFAULT_COOKIES_TXT = '{COOKIES_TXT}'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=DEFAULT_ENABLED, editable=True)
|
||||||
|
|
||||||
|
CMD = models.CharField(max_length=255, default=DEFAULT_CMD)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS)
|
||||||
|
TIMEOUT = models.CharField(max_length=255, default=DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
ALIASES = {
|
||||||
|
'ENABLED': (f'SAVE_{NAME}', f'USE_{NAME}', f'FETCH_{NAME}'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.LABEL} Extractor Configuration"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
verbose_name = f"Default Extractor Configuration"
|
||||||
|
app_label = 'defaults'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def dependency(self):
|
||||||
|
return self.DEPENDENCY.get_solo()
|
||||||
|
|
||||||
|
def __json__(self):
|
||||||
|
return {
|
||||||
|
'type': 'ArchiveBoxExtractor',
|
||||||
|
'__class__': self.__class__.__name__,
|
||||||
|
'NAME': self.NAME,
|
||||||
|
'LABEL': self.LABEL,
|
||||||
|
'ENABLED': self.ENABLED,
|
||||||
|
'DEPENDENCY': self.dependency.__json__(),
|
||||||
|
'ARGS': self.ARGS,
|
||||||
|
'CMD': self.CMD,
|
||||||
|
'TIMEOUT': self.TIMEOUT,
|
||||||
|
'is_valid': self.is_valid,
|
||||||
|
'is_enabled': self.is_enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_args(self, csv: List[str], **config):
|
||||||
|
un_prefixed_config = {**self.__json__()} # e.g. ENABLED=True
|
||||||
|
prefixed_config = { # e.g. GALLERYDL_ENABLED=True
|
||||||
|
f'{self.NAME}_{key}': value
|
||||||
|
for key, value in un_prefixed_config.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
merged_config = {
|
||||||
|
**config, # e.g. TIMEOUT=60
|
||||||
|
**un_prefixed_config, # e.g. ENABLED=True
|
||||||
|
**prefixed_config, # e.g. GALLERYDL_ENABLED=True
|
||||||
|
}
|
||||||
|
formatted_config = [
|
||||||
|
arg.format(**merged_config)
|
||||||
|
for arg in csv
|
||||||
|
]
|
||||||
|
|
||||||
|
return formatted_config
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_valid(self):
|
||||||
|
if not self.dependency.is_valid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# TIMEOUT must be at least 5 seconds
|
||||||
|
# if self.TIMEOUT < 5:
|
||||||
|
# return False
|
||||||
|
|
||||||
|
# assert Path(self.COOKIES_TXT).exists()
|
||||||
|
# TODO: validate user agent with uaparser
|
||||||
|
# TODO: validate args, cookies.txt?
|
||||||
|
return True
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_enabled(self):
|
||||||
|
return self.ENABLED and self.is_valid and self.dependency.is_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
assert self.is_valid
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
result = super().save(*args, **kwargs)
|
||||||
|
# post to message bus:
|
||||||
|
print({
|
||||||
|
'type': f'{self.__class__.__name__}.save',
|
||||||
|
'diff': self.__json__(),
|
||||||
|
'kwargs': kwargs,
|
||||||
|
})
|
||||||
|
# potential consumers of this event:
|
||||||
|
# - event logger: write to events.log
|
||||||
|
# - config file updater: writes to ArchiveBox.conf
|
||||||
|
# - supervisor: restarts relevant dependencies/extractors
|
||||||
|
# - etc...
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def out_dir(self, url: str, snapshot_dir: Path, config: ConfigDict):
|
||||||
|
return (snapshot_dir / self.NAME)
|
||||||
|
|
||||||
|
def create_out_dir(self, url: str, snapshot_dir: Path, config: ConfigDict):
|
||||||
|
out_dir = self.out_dir(url=url, snapshot_dir=snapshot_dir, config=config)
|
||||||
|
return out_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def should_extract(self, url: str, snapshot_dir: Path, config: ConfigDict):
|
||||||
|
# return False if extractor is disabled
|
||||||
|
if not self.is_enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
out_dir = self.out_dir(url=url, snapshot_dir=snapshot_dir, config=config)
|
||||||
|
|
||||||
|
if has_existing_output := out_dir.glob('*'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not (has_write_access := os.access(out_dir, os.W_OK | os.X_OK)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_dependency_cmd(self, url: str, extractor_dir: Path, config: ConfigDict):
|
||||||
|
return [
|
||||||
|
self.format_args(self.CMD, **config),
|
||||||
|
url,
|
||||||
|
*self.format_args(self.ARGS, **config), # TODO: split and requote this properly
|
||||||
|
]
|
||||||
|
|
||||||
|
# @requires_config('HOSTNAME', 'TIMEOUT', 'USER_AGENT', 'CHECK_SSL_VALIDITY')
|
||||||
|
def extract(self, url: str, snapshot_dir: Path, config: ConfigDict):
|
||||||
|
if not self.ENABLED:
|
||||||
|
return
|
||||||
|
|
||||||
|
extractor_dir = self.create_extractor_directory(snapshot_dir)
|
||||||
|
|
||||||
|
cmd = self.get_dependency_cmd(url=url, extractor_dir=extractor_dir, config=config)
|
||||||
|
|
||||||
|
status, stdout, stderr, output_path = 'failed', '', '', None
|
||||||
|
try:
|
||||||
|
proc, timer, errors = self.dependency.run(cmd, cwd=extractor_dir, timeout=self.TIMEOUT)
|
||||||
|
stdout, stderr = proc.stdout, proc.stderr
|
||||||
|
|
||||||
|
if 'ERROR: Unsupported URL' in stderr:
|
||||||
|
hints = ('gallery-dl doesnt support this type of url yet',)
|
||||||
|
raise ArchiveError('Failed to save gallerydl', hints)
|
||||||
|
|
||||||
|
if proc.returncode == 0 and 'finished' in stdout:
|
||||||
|
output_path = extractor_dir / 'index.html'
|
||||||
|
status = 'succeeded'
|
||||||
|
except Exception as err:
|
||||||
|
stderr += err
|
||||||
|
|
||||||
|
num_bytes, num_dirs, num_files = get_dir_size(extractor_dir)
|
||||||
|
|
||||||
|
return ArchiveResult(
|
||||||
|
cmd=cmd,
|
||||||
|
pwd=str(out_dir),
|
||||||
|
cmd_version=self.dependency.bin_version,
|
||||||
|
cmd_path=self.dependency.bin_path,
|
||||||
|
cmd_hostname=config.HOSTNAME,
|
||||||
|
|
||||||
|
output_path=output_path,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
status=status,
|
||||||
|
|
||||||
|
num_bytes=num_bytes,
|
||||||
|
num_files=num_files,
|
||||||
|
num_dirs=num_dirs,
|
||||||
|
**timer.stats,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveBoxDefaultExtractor(ArchiveBoxBaseExtractor, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(default=singleton_instance_id, primary_key=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'defaults'
|
12
archivebox/plugins/defaults/settings.py
Normal file
12
archivebox/plugins/defaults/settings.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
def register_plugin_settings(settings=settings, name='defaults'):
|
||||||
|
settings.STATICFILES_DIRS += [
|
||||||
|
str(Path(PACKAGE_DIR) / f'plugins/{name}/static'),
|
||||||
|
]
|
||||||
|
|
||||||
|
settings.TEMPLATE_DIRS += [
|
||||||
|
str(Path(PACKAGE_DIR) / f'plugins/{name}/templates'),
|
||||||
|
]
|
||||||
|
|
||||||
|
print('REGISTERED PLUGIN SETTINGS', name)
|
8
archivebox/plugins/gallerydl/admin.py
Normal file
8
archivebox/plugins/gallerydl/admin.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from solo.admin import SingletonModelAdmin
|
||||||
|
|
||||||
|
from .models import GalleryDLDependency, GalleryDLExtractor
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(GalleryDLDependency, SingletonModelAdmin)
|
||||||
|
admin.site.register(GalleryDLExtractor, SingletonModelAdmin)
|
|
@ -1,166 +1,93 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from solo.models import SingletonModel
|
from solo.models import SingletonModel
|
||||||
|
|
||||||
|
from archivebox.plugins.defaults.models import (
|
||||||
class GalleryDLDependency(SingletonModel):
|
ArchiveBoxDefaultDependency,
|
||||||
GALLERYDL_ENABLED = models.BooleanField(default=True)
|
ArchiveBoxDefaultExtractor,
|
||||||
GALLERYDL_BINARY = models.CharField(max_length=255, default='gallery-dl')
|
BashEnvironmentDependency,
|
||||||
|
PipEnvironmentDependency,
|
||||||
# GALLERYDL_WORKERS = models.IntegerField(default='{NUM_CORES}')
|
)
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
class GalleryDLDependency(ArchiveBoxDefaultDependency, SingletonModel):
|
||||||
return "GalleryDL Dependency Configuration"
|
NAME = 'GALLERYDL'
|
||||||
|
LABEL = "GalleryDL"
|
||||||
|
REQUIRED = False
|
||||||
|
|
||||||
class Meta:
|
PARENT_DEPENDENCIES = [
|
||||||
verbose_name = "GalleryDL Dependency Configuration"
|
BashEnvironmentDependency,
|
||||||
|
PipEnvironmentDependency,
|
||||||
|
]
|
||||||
|
|
||||||
@cached_property
|
BIN_DEPENDENCIES = ['gallery-dl']
|
||||||
def bin_path(self):
|
APT_DEPENDENCIES = []
|
||||||
return bin_path(self.GALLERYDL_BINARY)
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_PACKAGES = ['gallery-dl']
|
||||||
|
NPM_PACKAGES = []
|
||||||
|
|
||||||
@cached_property
|
DEFAULT_BINARY = 'gallery-dl'
|
||||||
def bin_version(self):
|
DEFAULT_START_CMD = None
|
||||||
return bin_version(self.bin_path)
|
DEFAULT_ARGS = []
|
||||||
|
VERSION_CMD = '{BINARY} --version'
|
||||||
|
|
||||||
@cached_property
|
ENABLED = models.BooleanField(default=True)
|
||||||
def is_valid(self):
|
BINARY = models.CharField(max_length=255, default='gallery-dl')
|
||||||
return self.bin_path and self.bin_version
|
|
||||||
|
|
||||||
@cached_property
|
WORKERS = models.IntegerField(default='1')
|
||||||
def enabled(self):
|
|
||||||
return self.GALLERYDL_ENABLED and self.is_valid
|
|
||||||
|
|
||||||
|
|
||||||
def run(args, pwd, timeout):
|
class GalleryDLExtractor(ArchiveBoxDefaultExtractor, SingletonModel):
|
||||||
errors = None
|
NAME = 'GALLERYDL'
|
||||||
timer = TimedProgress(timeout, prefix=' ')
|
LABEL = 'gallery-dl'
|
||||||
try:
|
|
||||||
proc = run(cmd=[self.bin_path, *args]=True, pwd=pwd, timeout=timeout)run(cmd=[self.bin_path, *args]=True, pwd=pwd, timeout=timeout)
|
|
||||||
|
|
||||||
except Exception as err:
|
DEPENDENCY = GalleryDLDependency.get_solo()
|
||||||
errors = err
|
|
||||||
finally:
|
|
||||||
timer.end()
|
|
||||||
|
|
||||||
return proc, timer, errors
|
|
||||||
|
|
||||||
|
|
||||||
def pretty_version(self):
|
|
||||||
if self.enabled:
|
|
||||||
if self.is_valid:
|
|
||||||
color, symbol, note, version = 'green', '√', 'valid', ''
|
|
||||||
|
|
||||||
parsed_version_num = re.search(r'[\d\.]+', self.bin_version)
|
|
||||||
if parsed_version_num:
|
|
||||||
version = f'v{parsed_version_num[0]}'
|
|
||||||
|
|
||||||
if not self.bin_version:
|
|
||||||
color, symbol, note, version = 'red', 'X', 'invalid', '?'
|
|
||||||
else:
|
|
||||||
color, symbol, note, version = 'lightyellow', '-', 'disabled', '-'
|
|
||||||
|
|
||||||
path = pretty_path(self.bin_path)
|
|
||||||
|
|
||||||
return ' '.join((
|
|
||||||
ANSI[color],
|
|
||||||
symbol,
|
|
||||||
ANSI['reset'],
|
|
||||||
name.ljust(21),
|
|
||||||
version.ljust(14),
|
|
||||||
ANSI[color],
|
|
||||||
note.ljust(8),
|
|
||||||
ANSI['reset'],
|
|
||||||
path.ljust(76),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GalleryDLExtractor(SingletonModel):
|
|
||||||
GALLERYDL_EXTRACTOR_NAME = 'gallerydl'
|
|
||||||
|
|
||||||
SAVE_GALLERYDL = models.BooleanField(default=True)
|
|
||||||
|
|
||||||
GALLERYDL_DEPENDENCY = GalleryDLDependency.get_solo()
|
|
||||||
|
|
||||||
# https://github.com/mikf/gallery-dl
|
# https://github.com/mikf/gallery-dl
|
||||||
GALLERYDL_ARGS = models.CSVField(max_length=255, default=[])
|
DEFAULT_CMD = [
|
||||||
GALLERYDL_TIMEOUT = models.IntegerField(default=lambda c: c['TIMEOUT'])
|
'{DEPENDENCY.BINARY}',
|
||||||
GALLERYDL_USER_AGENT = models.CharField(max_length=255, default='{USER_AGENT}')
|
'{ARGS}'
|
||||||
GALLERYDL_COOKIES_TXT = models.CharField(max_length=255, default='{COOKIES_TXT}')
|
'{url}',
|
||||||
|
]
|
||||||
|
DEFAULT_ARGS = [
|
||||||
|
'--timeout', self.TIMEOUT.format(**config),
|
||||||
|
'--cookies', self.COOKIES_TXT.format(**config),
|
||||||
|
'--user-agent', self.COOKIES_TXT.format(**config),
|
||||||
|
'--verify', self.CHECK_SSL_VALIDITY.format(**config),
|
||||||
|
]
|
||||||
|
|
||||||
ALIASES = {
|
ENABLED = models.BooleanField(default=True)
|
||||||
'SAVE_GALLERYDL': ('USE_GALLERYDL', 'FETCH_GALLERYDL'),
|
|
||||||
}
|
|
||||||
|
|
||||||
@cached_property
|
CMD = models.CharField(max_length=255, default=DEFAULT_CMD)
|
||||||
def enabled(self):
|
ARGS = models.CSVField(max_length=255, default=DEFAULT_ARGS)
|
||||||
return self.SAVE_GALLERYDL and self.GALLERYDL_DEPENDENCY.is_valid
|
|
||||||
|
|
||||||
|
TIMEOUT = models.CharField(max_length=255, default='{TIMEOUT}')
|
||||||
|
USER_AGENT = models.CharField(max_length=255, default='{USER_AGENT}')
|
||||||
|
COOKIES_TXT = models.CharField(max_length=255, default='{COOKIES_TXT}')
|
||||||
|
CHECK_SSL_VALIDITY = models.CharField(default='{CHECK_SSL_VALIDITY}')
|
||||||
|
|
||||||
def __str__(self):
|
# @task
|
||||||
return "GalleryDL Extractor Configuration"
|
# @requires_config('HOSTNAME', 'TIMEOUT', 'USER_AGENT', 'CHECK_SSL_VALIDITY')
|
||||||
|
def extract(self, url: str, out_dir: Path, config: ConfigDict):
|
||||||
class Meta:
|
if not self.ENABLED:
|
||||||
verbose_name = "GalleryDL Extractor Configuration"
|
|
||||||
|
|
||||||
def __json__(self):
|
|
||||||
return {
|
|
||||||
'SAVE_GALLERYDL': self.SAVE_GALLERYDL,
|
|
||||||
'GALLERYDL_DEPENDENCY': self.GALLERYDL_DEPENDENCY.__json__(),
|
|
||||||
'GALLERYDL_ARGS': self.GALLERYDL_ARGS,
|
|
||||||
'GALLERYDL_TIMEOUT': self.GALLERYDL_TIMEOUT,
|
|
||||||
'GALLERYDL_USER_AGENT': self.GALLERYDL_USER_AGENT,
|
|
||||||
'GALLERYDL_COOKIES_TXT': self.GALLERYDL_COOKIES_TXT,
|
|
||||||
}
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
assert 5 < self.GALLERYDL_TIMEOUT, 'GALLERYDL_TIMEOUT must be at least 5 seconds'
|
|
||||||
# assert Path(self.GALLERYDL_COOKIES_TXT).exists()
|
|
||||||
# TODO: validate user agent with uaparser
|
|
||||||
# TODO: validate args, cookies.txt?
|
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.validate()
|
|
||||||
with transaction.atomic():
|
|
||||||
result = super().save(*args, **kwargs)
|
|
||||||
emit_event({'type': 'GalleryDLExtractor.save', 'diff': self.__json__(), 'kwargs': kwargs})
|
|
||||||
# potential consumers of this event:
|
|
||||||
# - event logger: write to events.log
|
|
||||||
# - config file updater: writes to ArchiveBox.conf
|
|
||||||
# - supervisor: restarts relevant dependencies/extractors
|
|
||||||
# - etc...
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def create_extractor_directory(self, parent_dir: Path):
|
|
||||||
return subdir = (parent_dir / self.GALLERYDL_EXTRACTOR_NAME).mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
def should_extract(self, parent_dir: Path):
|
|
||||||
existing_files = (parent_dir / self.GALLERYDL_EXTRACTOR_NAME).glob('*')
|
|
||||||
return not existing_files
|
|
||||||
|
|
||||||
|
|
||||||
def extract(self, url: str, out_dir: Path):
|
|
||||||
if not self.enabled:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
extractor_dir = self.create_extractor_directory(out_dir)
|
extractor_dir = self.create_extractor_directory(out_dir)
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
self.GALLERYDL_DEPENDENCY.bin_path,
|
self.CMD,
|
||||||
url,
|
url,
|
||||||
'--timeout', GALLERYDL_TIMEOUT,
|
'--timeout', self.TIMEOUT.format(**config),
|
||||||
'--cookies', GALLERYDL_COOKIES_TXT,
|
'--cookies', self.COOKIES_TXT.format(**config),
|
||||||
'--user-agent', GALLERYDL_USER_AGENT,
|
'--user-agent', self.COOKIES_TXT.format(**config),
|
||||||
'--verify', config.CHECK_SSL_VALIDITY
|
'--verify', self.CHECK_SSL_VALIDITY.format(**config),
|
||||||
*self.GALLERYDL_ARGS,
|
*split_args(self.ARGS.format(**config)),
|
||||||
]
|
]
|
||||||
|
|
||||||
status, stdout, stderr, output_path = 'failed', '', '', None
|
status, stdout, stderr, output_path = 'failed', '', '', None
|
||||||
try:
|
try:
|
||||||
proc, timer, errors = self.GALLERYDL_DEPENDENCY.run(cmd, cwd=extractor_dir, timeout=self.GALLERYDL_TIMEOUT)
|
proc, timer, errors = self.DEPENDENCY.run(cmd, cwd=extractor_dir, timeout=self.GALLERYDL_TIMEOUT)
|
||||||
stdout, stderr = proc.stdout, proc.stderr
|
stdout, stderr = proc.stdout, proc.stderr
|
||||||
|
|
||||||
if 'ERROR: Unsupported URL' in stderr:
|
if 'ERROR: Unsupported URL' in stderr:
|
||||||
|
@ -176,17 +103,16 @@ class GalleryDLExtractor(SingletonModel):
|
||||||
num_bytes, num_dirs, num_files = get_dir_size(extractor_dir)
|
num_bytes, num_dirs, num_files = get_dir_size(extractor_dir)
|
||||||
|
|
||||||
return ArchiveResult(
|
return ArchiveResult(
|
||||||
status=status,
|
|
||||||
|
|
||||||
cmd=cmd,
|
cmd=cmd,
|
||||||
pwd=str(out_dir),
|
pwd=str(out_dir),
|
||||||
cmd_version=self.GALLERYDL_DEPENDENCY.bin_version,
|
cmd_version=self.DEPENDENCY.bin_version,
|
||||||
cmd_path=self.GALLERYDL_DEPENDENCY.bin_path,
|
cmd_path=self.DEPENDENCY.bin_path,
|
||||||
cmd_hostname=config.HOSTNAME,
|
cmd_hostname=config.HOSTNAME,
|
||||||
|
|
||||||
output_path=output_path,
|
output_path=output_path,
|
||||||
stdout=stdout,
|
stdout=stdout,
|
||||||
stderr=stderr,
|
stderr=stderr,
|
||||||
|
status=status,
|
||||||
|
|
||||||
num_bytes=num_bytes,
|
num_bytes=num_bytes,
|
||||||
num_files=num_files,
|
num_files=num_files,
|
||||||
|
|
59
archivebox/plugins/gallerydl/plugin.yaml
Normal file
59
archivebox/plugins/gallerydl/plugin.yaml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
dependencies:
|
||||||
|
GalleryDLDependency:
|
||||||
|
ID: gallerydl
|
||||||
|
LABEL: GalleryDL
|
||||||
|
REQUIRED: false
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES:
|
||||||
|
- BashEnvironmentDependency
|
||||||
|
- PipEnvironmentDependency
|
||||||
|
|
||||||
|
PIP_DEPENDENCIES:
|
||||||
|
- gallery-dl
|
||||||
|
|
||||||
|
USER_CONFIG:
|
||||||
|
ENABLED: models.BooleanField(max_length=255, default={DEFAULT_CONFIG.ENABLED})
|
||||||
|
BINARY: models.CharField(max_length=255, default={DEFAULT_CONFIG.BINARY})
|
||||||
|
|
||||||
|
DEFAULT_CONFIG:
|
||||||
|
ENABLED: true
|
||||||
|
BINARY: 'gallery-dl'
|
||||||
|
|
||||||
|
CONFIG_ALIASES:
|
||||||
|
- SAVE_GALLERYDL: ENABLED
|
||||||
|
- USE_GALLERYDL: ENABLED
|
||||||
|
- GALLERYDL_ENABLED: ENABLED
|
||||||
|
- GALLERYDL_BINARY: BINARY
|
||||||
|
|
||||||
|
TASKS:
|
||||||
|
# plugins.GalleryDLDependency
|
||||||
|
run_dependency: plugins.gallerydl.models.GalleryDLDependency.run_dependency
|
||||||
|
|
||||||
|
|
||||||
|
extractors:
|
||||||
|
GalleryDLExtractor:
|
||||||
|
ID: GALLERYDL
|
||||||
|
LABEL: GalleryDL
|
||||||
|
ENABLED: true
|
||||||
|
|
||||||
|
DEPENDENCY: GalleryDLDependency
|
||||||
|
|
||||||
|
CONFIG:
|
||||||
|
ENABLED: models.BooleanField(default={DEFAULT_CONFIG.ENABLED})
|
||||||
|
CMD: models.CharField(max_length=255, default={DEFAULT_CONFIG.CMD})
|
||||||
|
ARGS: models.CharField(max_length=255, default={DEFAULT_CONFIG.ARGS})
|
||||||
|
USER_AGENT: models.CharField(max_length=255, default={DEFAULT_CONFIG.USER_AGENT})
|
||||||
|
CHECK_SSL_VALIDITY: models.CharField(max_length=255, default={DEFAULT_CONFIG.CHECK_SSL_VALIDITY})
|
||||||
|
|
||||||
|
DEFAULT_CONFIG:
|
||||||
|
ENABLED: true
|
||||||
|
CMD: gallery-dl {args} {url}
|
||||||
|
ARGS: --user-agent={USER_AGENT} --check-ssl={CHECK_SSL_VALIDITY}
|
||||||
|
CHECK_SSL_VALIDITY: {CHECK_SSL_VALIDITY}
|
||||||
|
USER_AGENT: {USER_AGENT}
|
||||||
|
|
||||||
|
|
||||||
|
TASKS:
|
||||||
|
CREATE_OUT_DIR: plugins.gallerydl.tasks.create_out_dir
|
||||||
|
SHOULD_EXTRACT: plugins.gallerydl.tasks.should_extract
|
||||||
|
EXTRACT: plugins.gallerydl.tasks.extract
|
34
archivebox/plugins/system/admin.py
Normal file
34
archivebox/plugins/system/admin.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from solo.admin import SingletonModelAdmin
|
||||||
|
|
||||||
|
from plugins.defaults.admin import DependencyAdmin, ExtractorAdmin
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
BashEnvironmentDependency,
|
||||||
|
AptEnvironmentDependency,
|
||||||
|
BrewEnvironmentDependency,
|
||||||
|
PipEnvironmentDependency,
|
||||||
|
NPMEnvironmentDependency,
|
||||||
|
|
||||||
|
SQLiteDependency,
|
||||||
|
DjangoDependency,
|
||||||
|
ArchiveBoxDependency,
|
||||||
|
|
||||||
|
# ArchiveBoxDefaultExtractor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print('DefaultsPluginConfig.admin')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(BashEnvironmentDependency, DependencyAdmin)
|
||||||
|
admin.site.register(AptEnvironmentDependency, DependencyAdmin)
|
||||||
|
admin.site.register(BrewEnvironmentDependency, DependencyAdmin)
|
||||||
|
admin.site.register(PipEnvironmentDependency, DependencyAdmin)
|
||||||
|
admin.site.register(NPMEnvironmentDependency, DependencyAdmin)
|
||||||
|
|
||||||
|
admin.site.register(SQLiteDependency, DependencyAdmin)
|
||||||
|
admin.site.register(DjangoDependency, DependencyAdmin)
|
||||||
|
admin.site.register(ArchiveBoxDependency, DependencyAdmin)
|
||||||
|
|
||||||
|
# admin.site.register(ArchiveBoxDefaultExtractor, ExtractorAdmin)
|
21
archivebox/plugins/system/apps.py
Normal file
21
archivebox/plugins/system/apps.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# __package__ = 'archivebox.plugins.system'
|
||||||
|
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SystemPluginConfig(AppConfig):
|
||||||
|
label = "ArchiveBox System"
|
||||||
|
name = "system"
|
||||||
|
|
||||||
|
default_auto_field = "django.db.models.AutoField"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
print('plugins.system.apps.SystemPluginConfig.ready')
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .settings import register_plugin_settings
|
||||||
|
|
||||||
|
register_plugin_settings(settings, name=self.name)
|
||||||
|
|
110
archivebox/plugins/system/migrations/0001_initial.py
Normal file
110
archivebox/plugins/system/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# Generated by Django 3.1.14 on 2024-01-24 08:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AptEnvironmentDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True)),
|
||||||
|
('BINARY', models.CharField(default='apt-get', max_length=255)),
|
||||||
|
('ARGS', models.CharField(default='-qq', max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ArchiveBoxDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True, editable=False)),
|
||||||
|
('BINARY', models.CharField(default='archivebox', editable=False, max_length=255)),
|
||||||
|
('ARGS', models.CharField(default=[], editable=False, max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BashEnvironmentDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True, editable=False)),
|
||||||
|
('BINARY', models.CharField(default='bash', max_length=255)),
|
||||||
|
('ARGS', models.CharField(default='-c', max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BrewEnvironmentDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True)),
|
||||||
|
('BINARY', models.CharField(default='brew', max_length=255)),
|
||||||
|
('ARGS', models.CharField(default='', max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DjangoDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True, editable=False)),
|
||||||
|
('BINARY', models.CharField(default='django-admin.py', editable=False, max_length=255)),
|
||||||
|
('ARGS', models.CharField(default=[], editable=False, max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='NPMEnvironmentDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True)),
|
||||||
|
('BINARY', models.CharField(default='node', max_length=255)),
|
||||||
|
('ARGS', models.CharField(default='', max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PipEnvironmentDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True)),
|
||||||
|
('BINARY', models.CharField(default='pip3', max_length=255)),
|
||||||
|
('ARGS', models.CharField(default='', max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SQLiteDependency',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('ENABLED', models.BooleanField(default=True, editable=False)),
|
||||||
|
('BINARY', models.CharField(default='sqlite3', editable=False, max_length=255)),
|
||||||
|
('ARGS', models.CharField(default=[], editable=False, max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
archivebox/plugins/system/migrations/__init__.py
Normal file
0
archivebox/plugins/system/migrations/__init__.py
Normal file
361
archivebox/plugins/system/models.py
Normal file
361
archivebox/plugins/system/models.py
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
# __package__ = 'archivebox.plugins.system'
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import django
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
from solo.models import SingletonModel
|
||||||
|
|
||||||
|
from plugins.defaults.models import ArchiveBoxBaseDependency, bin_path, bin_version
|
||||||
|
|
||||||
|
ConfigDict = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class BashEnvironmentDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'BASH'
|
||||||
|
LABEL = "Bash"
|
||||||
|
REQUIRED = True
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = []
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = ['bash']
|
||||||
|
APT_DEPENDENCIES = []
|
||||||
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_DEPENDENCIES = []
|
||||||
|
NPM_DEPENDENCIES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'bash'
|
||||||
|
DEFAULT_START_CMD = None
|
||||||
|
DEFAULT_STOP_CMD = None
|
||||||
|
DEFAULT_PID_FILE = None
|
||||||
|
DEFAULT_ARGS = '-c'
|
||||||
|
VERSION_CMD = '{BINARY} --version'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=not REQUIRED)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS)
|
||||||
|
|
||||||
|
# START_CMD = models.CharField(max_length=255, default=DEFAULT_START_CMD)
|
||||||
|
# WORKERS = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
# @task
|
||||||
|
def install_pkgs(self, os_pkgs=()):
|
||||||
|
assert self.is_valid, 'Bash environment is not available on this host'
|
||||||
|
|
||||||
|
for os_dependency in os_pkgs:
|
||||||
|
assert bin_path(os_dependency)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
class AptEnvironmentDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'APT'
|
||||||
|
LABEL = "apt"
|
||||||
|
REQUIRED = False
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = [BashEnvironmentDependency]
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = ['apt-get']
|
||||||
|
APT_DEPENDENCIES = []
|
||||||
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_PACKAGES = []
|
||||||
|
NPM_PACKAGES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'apt-get'
|
||||||
|
DEFAULT_START_CMD = None
|
||||||
|
DEFAULT_STOP_CMD = None
|
||||||
|
DEFAULT_PID_FILE = None
|
||||||
|
DEFAULT_ARGS = '-qq'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=not REQUIRED)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
# @task
|
||||||
|
def install_pkgs(self, apt_pkgs=()):
|
||||||
|
assert self.is_valid, 'Apt environment is not available on this host'
|
||||||
|
|
||||||
|
run(cmd=[self.DEFAULT_BINARY, '-qq', 'update'])
|
||||||
|
for apt_package in apt_pkgs:
|
||||||
|
run(cmd=[self.DEFAULT_BINARY, 'install', '-y', apt_package])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
class BrewEnvironmentDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'BREW'
|
||||||
|
LABEL = "homebrew"
|
||||||
|
REQUIRED = False
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = [BashEnvironmentDependency]
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = ['brew']
|
||||||
|
APT_DEPENDENCIES = []
|
||||||
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_PACKAGES = []
|
||||||
|
NPM_PACKAGES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'brew'
|
||||||
|
DEFAULT_START_CMD = None
|
||||||
|
DEFAULT_STOP_CMD = None
|
||||||
|
DEFAULT_PID_FILE = None
|
||||||
|
DEFAULT_ARGS = ''
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=True)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
# @task
|
||||||
|
def install_pkgs(self, brew_pkgs=()):
|
||||||
|
assert self.is_valid, 'Brw environment is not available on this host'
|
||||||
|
|
||||||
|
run(cmd=[self.DEFAULT_BINARY, 'update'])
|
||||||
|
|
||||||
|
for brew_pkg in brew_pkgs:
|
||||||
|
run(cmd=[self.DEFAULT_BINARY, 'install', brew_pkg])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PipEnvironmentDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'PIP'
|
||||||
|
LABEL = "pip"
|
||||||
|
REQUIRED = False
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = [BashEnvironmentDependency]
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = ['python3', 'pip3']
|
||||||
|
APT_DEPENDENCIES = ['python3.11', 'pip3', 'pipx']
|
||||||
|
BREW_DEPENDENCIES = ['python@3.11', 'pipx']
|
||||||
|
PIP_PACKAGES = ['setuptools', 'pipx']
|
||||||
|
NPM_PACKAGES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'pip3'
|
||||||
|
DEFAULT_START_CMD = None
|
||||||
|
DEFAULT_STOP_CMD = None
|
||||||
|
DEFAULT_PID_FILE = None
|
||||||
|
DEFAULT_ARGS = ''
|
||||||
|
VERSION_CMD = '{BINARY} --version'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=True)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
# @task
|
||||||
|
def install_pkgs(self, pip_pkgs=()):
|
||||||
|
assert self.is_valid, 'Pip environment is not available on this host'
|
||||||
|
|
||||||
|
for pip_pkg in pip_pkgs:
|
||||||
|
run(cmd=[self.DEFAULT_BINARY, 'install', '--update', '--ignore-installed', pip_pkg])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class NPMEnvironmentDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'NODEJS'
|
||||||
|
LABEL = "NodeJS"
|
||||||
|
REQUIRED = False
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = [BashEnvironmentDependency]
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = ['node', 'npm']
|
||||||
|
APT_DEPENDENCIES = ['node', 'npm']
|
||||||
|
BREW_DEPENDENCIES = ['node', 'npm']
|
||||||
|
PIP_PACKAGES = []
|
||||||
|
NPM_PACKAGES = ['npm']
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'node'
|
||||||
|
DEFAULT_START_CMD = None
|
||||||
|
DEFAULT_STOP_CMD = None
|
||||||
|
DEFAULT_PID_FILE = None
|
||||||
|
DEFAULT_ARGS = ''
|
||||||
|
VERSION_CMD = '{BINARY} --version'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=True)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
# @task
|
||||||
|
def install_pkgs(self, npm_pkgs=()):
|
||||||
|
assert self.is_valid, 'NPM environment is not available on this host'
|
||||||
|
|
||||||
|
for npm_pkg in npm_pkgs:
|
||||||
|
run(cmd=[self.DEFAULT_BINARY, 'install', npm_pkg])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'DJANGO'
|
||||||
|
LABEL = "Django"
|
||||||
|
REQUIRED = True
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = []
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = ['django-admin.py']
|
||||||
|
APT_DEPENDENCIES = []
|
||||||
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_PACKAGES = ['django==3.1.14']
|
||||||
|
NPM_PACKAGES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'django-admin.py'
|
||||||
|
DEFAULT_START_CMD = 'archivebox server 0.0.0.0:8000'
|
||||||
|
DEFAULT_PID_FILE = 'logs/{NAME}_WORKER.pid'
|
||||||
|
DEFAULT_STOP_CMD = 'kill "$(<{PID_FILE})"'
|
||||||
|
DEFAULT_ARGS = []
|
||||||
|
VERSION_CMD = '{BINARY} --version'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=False)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY, editable=False)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_path(self):
|
||||||
|
return inspect.getfile(django)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_version(self):
|
||||||
|
return django.VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class SQLiteDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'SQLITE'
|
||||||
|
LABEL = "SQLite"
|
||||||
|
REQUIRED = True
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = []
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = []
|
||||||
|
APT_DEPENDENCIES = []
|
||||||
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_PACKAGES = []
|
||||||
|
NPM_PACKAGES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'sqlite3'
|
||||||
|
DEFAULT_START_CMD = None
|
||||||
|
DEFAULT_STOP_CMD = None
|
||||||
|
DEFAULT_PID_FILE = None
|
||||||
|
DEFAULT_ARGS = []
|
||||||
|
VERSION_CMD = 'python3 -c ""'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=False)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY, editable=False)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_path(self):
|
||||||
|
return inspect.getfile(sqlite3)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_version(self):
|
||||||
|
return sqlite3.version
|
||||||
|
|
||||||
|
class ArchiveBoxDependency(ArchiveBoxBaseDependency, SingletonModel):
|
||||||
|
singleton_instance_id = 1
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
NAME = 'ARCHIVEBOX'
|
||||||
|
LABEL = "ArchiveBox"
|
||||||
|
REQUIRED = True
|
||||||
|
|
||||||
|
PARENT_DEPENDENCIES = [
|
||||||
|
PipEnvironmentDependency,
|
||||||
|
DjangoDependency,
|
||||||
|
SQLiteDependency,
|
||||||
|
]
|
||||||
|
|
||||||
|
BIN_DEPENDENCIES = ['archivebox']
|
||||||
|
APT_DEPENDENCIES = []
|
||||||
|
BREW_DEPENDENCIES = []
|
||||||
|
PIP_PACKAGES = ['archivebox']
|
||||||
|
NPM_PACKAGES = []
|
||||||
|
|
||||||
|
DEFAULT_BINARY = 'archivebox'
|
||||||
|
DEFAULT_START_CMD = '{BINARY} server 0.0.0.0:8000'
|
||||||
|
DEFAULT_ARGS = []
|
||||||
|
VERSION_CMD = 'archivebox --version'
|
||||||
|
|
||||||
|
ENABLED = models.BooleanField(default=True, editable=False)
|
||||||
|
BINARY = models.CharField(max_length=255, default=DEFAULT_BINARY, editable=False)
|
||||||
|
ARGS = models.CharField(max_length=255, default=DEFAULT_ARGS, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
app_label = 'system'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_path(self):
|
||||||
|
return sys.argv[0] or bin_path('archivebox')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bin_version(self):
|
||||||
|
# return config['VERSION']
|
||||||
|
return '0.7.3+editable'
|
||||||
|
|
3
archivebox/plugins/system/settings.py
Normal file
3
archivebox/plugins/system/settings.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from plugins.defaults import register_plugin_settings
|
Loading…
Add table
Add a link
Reference in a new issue