Start implementation of file replication using ContentMonster
This commit is contained in:
parent
3c8c0dce25
commit
a3f28a5b85
10 changed files with 313 additions and 1 deletions
0
core/classes/__init__.py
Normal file
0
core/classes/__init__.py
Normal file
147
core/classes/replication.py
Normal file
147
core/classes/replication.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import sqlite3
|
||||||
|
import pathlib
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from typing import Union, Optional
|
||||||
|
|
||||||
|
from contentmonster.classes.file import File as ContentMonsterFile
|
||||||
|
from contentmonster.classes.directory import Directory as ContentMonsterDirectory
|
||||||
|
from contentmonster.classes.vessel import Vessel as ContentMonsterVessel
|
||||||
|
|
||||||
|
from ..models.replication import ReplicationFile, ReplicationSource, ReplicationFileLog, ReplicationTarget
|
||||||
|
|
||||||
|
|
||||||
|
class ContentMonsterDatabase:
|
||||||
|
"""Class wrapping Django database for ContentMonster
|
||||||
|
"""
|
||||||
|
|
||||||
|
def commit(self) -> None:
|
||||||
|
"""noop
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getFileUUID(self, fileobj: ContentMonsterFile) -> str:
|
||||||
|
"""Retrieve unique identifier for ContentMonsterFile object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fileobj (ContentMonsterFile): ContentMonsterFile object to retrieve UUID for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: UUID for passed ContentMonsterFile object
|
||||||
|
"""
|
||||||
|
hash = fileobj.getHash()
|
||||||
|
|
||||||
|
files = ReplicationFile.objects.filter(directory__name=fileobj.directory.name, name=fileobj.name)
|
||||||
|
|
||||||
|
# If file with same name and directory exists
|
||||||
|
for result in files:
|
||||||
|
|
||||||
|
# If it has the same hash, it is the same file -> return its UUID
|
||||||
|
if file.checksum == hash:
|
||||||
|
fileuuid = result.uuid
|
||||||
|
|
||||||
|
# If not, it is a file that can no longer exist -> delete it
|
||||||
|
else:
|
||||||
|
self.removeFileByUUID(result.uuid)
|
||||||
|
|
||||||
|
# Return found UUID or generate a new one
|
||||||
|
return fileuuid or self.addFile(fileobj, hash)
|
||||||
|
|
||||||
|
def addFile(self, fileobj: ContentMonsterFile, hash: Optional[str] = None) -> str:
|
||||||
|
"""Adds a new ReplicationFile object to the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fileobj (ContentMonsterFile): ContentMonsterFile object to add to database
|
||||||
|
hash (str, optional): Checksum of the file, if already known.
|
||||||
|
Defaults to None and will use .getHash() to calculate checksum then.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: UUID of the new ContentMonsterFile record
|
||||||
|
"""
|
||||||
|
hash = hash or fileobj.getHash()
|
||||||
|
fileuuid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
directory = ReplicationSource.objects.get(name=fileobj.directory.name)
|
||||||
|
ReplicationFile.objects.create(uuid=fileuuid, directory=directory, name=fileobj.name, checksum=hash)
|
||||||
|
|
||||||
|
return fileuuid
|
||||||
|
|
||||||
|
def getFileByUUID(self, fileuuid: str) -> Optional[tuple[str, str, str]]:
|
||||||
|
"""Get additional information on a ContentMonsterFile by its UUID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fileuuid (str): The UUID of the ReplicationFile to retrieve from the database
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple consisting of (directory, name, checksum), where
|
||||||
|
"directory" is the name of the Directory object the File is
|
||||||
|
located in, "name" is the filename (basename) of the File and
|
||||||
|
checksum is the SHA256 hash of the file at the time of insertion
|
||||||
|
into the database. None is returned if no such record is found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = ReplicationFile.objects.get(uuid=fileuuid)
|
||||||
|
return (result.directory.name, result.name, result.checksum)
|
||||||
|
except ReplicationFile.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def removeFile(self, directory: ContentMonsterDirectory, name: str) -> None:
|
||||||
|
"""Remove a ReplicationFile from the database based on ContentMonsterDirectory and filename
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory (ContentMonsterDirectory): ContentMonsterDirectory object
|
||||||
|
containing the ContentMonsterFile to remove
|
||||||
|
name (str): Filename of the ContentMonsterFile to remove
|
||||||
|
"""
|
||||||
|
ReplicationFile.objects.filter(directory__name=directory.name, name=name).delete()
|
||||||
|
|
||||||
|
def removeFileByUUID(self, fileuuid: str) -> None:
|
||||||
|
"""Remove a ReplicationFile from the database based on UUID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fileuuid (str): The UUID of the ContentMonsterFile to remove from the database
|
||||||
|
"""
|
||||||
|
ReplicationFile.objects.filter(uuid=fileuuid).delete()
|
||||||
|
|
||||||
|
def logCompletion(self, file: ContentMonsterFile, vessel: ContentMonsterVessel):
|
||||||
|
"""Log the completion of a ContentMonsterFile upload
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file (ContentMonsterFile): The ContentMonsterFile object that has been uploaded
|
||||||
|
vessel (ContentMonsterVessel): The ContentMonsterVessel the File has been
|
||||||
|
uploaded to
|
||||||
|
"""
|
||||||
|
fileobj = ReplicationFile.objects.get(uuid=file.uuid)
|
||||||
|
vesselobj = ReplicationTarget.objects.get(name=vessel.name)
|
||||||
|
ReplicationFileLog.objects.create(file=fileobj, vessel=vesselobj)
|
||||||
|
|
||||||
|
def getCompletionForVessel(self, vessel: ContentMonsterVessel) -> list[Optional[str]]:
|
||||||
|
"""Get completed uploads for a vessel
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vessel (ContentMonsterVessel): The ContentMonsterVessel object to retrieve
|
||||||
|
uploaded files for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of UUIDs of ContentMonsterFiles that have been successfully uploaded
|
||||||
|
"""
|
||||||
|
|
||||||
|
vesselobj = ReplicationTarget.objects.get(name=vessel.name)
|
||||||
|
objects = ReplicationFileLog.objects.filter(vessel=vesselobj)
|
||||||
|
|
||||||
|
return [o.file.uuid for o in objects]
|
||||||
|
|
||||||
|
def getCompletionByFileUUID(self, fileuuid: str) -> list[Optional[str]]:
|
||||||
|
objects = ReplicationFileLog.objects.filter(file__uuid=fileuuid)
|
||||||
|
|
||||||
|
return [o.vessel.name for o in objects]
|
||||||
|
|
||||||
|
def migrate(self) -> None:
|
||||||
|
"""noop
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""noop
|
||||||
|
"""
|
||||||
|
pass
|
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
51
core/management/commands/filereplication.py
Normal file
51
core/management/commands/filereplication.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from ...models.replication import ReplicationSource, ReplicationTarget
|
||||||
|
from ...classes.replication import ContentMonsterDatabase
|
||||||
|
|
||||||
|
from contentmonster.classes.config import MonsterConfig
|
||||||
|
from contentmonster.classes.vesselthread import VesselThread
|
||||||
|
from contentmonster.classes.shorethread import ShoreThread
|
||||||
|
|
||||||
|
from multiprocessing import Manager
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Runs the file replication service (ContentMonster)'
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
config = MonsterConfig()
|
||||||
|
|
||||||
|
for source in ReplicationSource.objects.all():
|
||||||
|
config.directories.append(source.to_directory())
|
||||||
|
|
||||||
|
for target in ReplicationTarget.objects.all():
|
||||||
|
config.vessels.append(target.to_vessel(dbclass=ContentMonsterDatabase))
|
||||||
|
|
||||||
|
with Manager() as manager:
|
||||||
|
state = manager.dict()
|
||||||
|
state["files"] = manager.list()
|
||||||
|
state["config"] = config
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
|
||||||
|
for vessel in config.vessels:
|
||||||
|
thread = VesselThread(vessel, state, dbclass=ContentMonsterDatabase)
|
||||||
|
thread.start()
|
||||||
|
threads.append(thread)
|
||||||
|
|
||||||
|
shore = ShoreThread(state, dbclass=ContentMonsterDatabase)
|
||||||
|
shore.start()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
time.sleep(10)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Keyboard interrupt received - stopping threads")
|
||||||
|
shore.terminate()
|
||||||
|
for thread in threads:
|
||||||
|
thread.terminate()
|
||||||
|
exit()
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Generated by Django 4.1.1 on 2022-09-20 05:05
|
||||||
|
|
||||||
|
import core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0006_alter_vessel_imo_alter_vessel_mmsi'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ReplicationFile',
|
||||||
|
fields=[
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('checksum', models.CharField(max_length=64)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ReplicationSource',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('location', models.CharField(max_length=2048, validators=[core.validators.validate_directory])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ReplicationTarget',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('address', models.CharField(max_length=256)),
|
||||||
|
('username', models.CharField(blank=True, default='code', max_length=64, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ReplicationFileLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.replicationfile')),
|
||||||
|
('vessel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.replicationtarget')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='replicationfile',
|
||||||
|
name='directory',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.replicationsource'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1 +1,3 @@
|
||||||
from .auth import User, OTPSession
|
from .auth import User, OTPSession
|
||||||
|
from .vessel import Vessel
|
||||||
|
from .replication import ReplicationFile, ReplicationFileLog, ReplicationSource, ReplicationTarget
|
48
core/models/replication.py
Normal file
48
core/models/replication.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from contentmonster.classes.directory import Directory
|
||||||
|
from contentmonster.classes.vessel import Vessel
|
||||||
|
|
||||||
|
from ..validators import validate_directory
|
||||||
|
|
||||||
|
from getpass import getuser
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class ReplicationSource(models.Model):
|
||||||
|
"""Represents Directory objects in ContentMonster"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
location = models.CharField(max_length=2048, validators=[validate_directory])
|
||||||
|
|
||||||
|
def to_directory(self) -> Directory:
|
||||||
|
return Directory(name, location)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplicationTarget(models.Model):
|
||||||
|
"""Represents Vessel objects in ContentMonster"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
address = models.CharField(max_length=256)
|
||||||
|
username = models.CharField(max_length=64, default=getuser(), null=True, blank=True)
|
||||||
|
|
||||||
|
def to_vessel(self, dbclass=None) -> Vessel:
|
||||||
|
return Vessel(name, address, username, dbclass=ContentMonsterDatabase)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplicationFile(models.Model):
|
||||||
|
"""Represents File objects in ContentMonster"""
|
||||||
|
|
||||||
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||||
|
directory = models.ForeignKey(ReplicationSource, models.CASCADE)
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
checksum = models.CharField(max_length=64)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplicationFileLog(models.Model):
|
||||||
|
"""Represents File completion in ContentMonster"""
|
||||||
|
|
||||||
|
file = models.ForeignKey(ReplicationFile, models.CASCADE)
|
||||||
|
vessel = models.ForeignKey(ReplicationTarget, models.CASCADE)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
8
core/validators.py
Normal file
8
core/validators.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def validate_directory(value):
|
||||||
|
if not Path(value).is_dir():
|
||||||
|
raise ValidationError(f"{value} is not a directory")
|
|
@ -11,6 +11,7 @@ django-crispy-forms
|
||||||
dbsettings
|
dbsettings
|
||||||
django-autosecretkey
|
django-autosecretkey
|
||||||
pycruisemapper
|
pycruisemapper
|
||||||
|
contentmonster
|
||||||
|
|
||||||
git+https://kumig.it/kumisystems/reportmonster.git
|
git+https://kumig.it/kumisystems/reportmonster.git
|
||||||
git+https://kumig.it/kumisystems/pyadonis.git
|
git+https://kumig.it/kumisystems/pyadonis.git
|
Loading…
Reference in a new issue