135 lines
7.3 KiB
Python
135 lines
7.3 KiB
Python
"""GoodDrive's logical folders and delegated user spaces.
|
|
|
|
A DriveSpace is a numbered physical directory. DriveItems remain DB-managed,
|
|
so renaming a space never renames its physical path.
|
|
"""
|
|
import uuid
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.core.exceptions import ValidationError
|
|
|
|
|
|
class DriveSpace(models.Model):
|
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='drive_space', null=True, blank=True)
|
|
managed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name='managed_drive_spaces')
|
|
private_owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='private_drive_space', null=True, blank=True)
|
|
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.PROTECT, related_name='children')
|
|
directory_name = models.CharField(max_length=24, unique=True, null=True, blank=True)
|
|
relative_path = models.CharField(max_length=700, unique=True)
|
|
display_name = models.CharField(max_length=120)
|
|
can_view_parent = models.BooleanField(default=True)
|
|
can_write_parent = models.BooleanField(default=False)
|
|
parent_permissions = models.JSONField(null=True, blank=True, default=None)
|
|
can_invite = models.BooleanField(default=False)
|
|
quota_bytes = models.BigIntegerField(null=True, blank=True, default=None)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return f'{self.display_name} ({self.relative_path})'
|
|
|
|
|
|
class SpaceGrant(models.Model):
|
|
"""Grant access to an EXISTING account, without changing its hierarchy."""
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='granted_spaces')
|
|
space = models.ForeignKey(DriveSpace, on_delete=models.CASCADE, related_name='access_grants')
|
|
can_write = models.BooleanField(default=False)
|
|
can_invite = models.BooleanField(default=False)
|
|
permissions = models.JSONField(null=True, blank=True, default=None)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
constraints = [models.UniqueConstraint(fields=['user', 'space'], name='drive_unique_space_grant')]
|
|
|
|
|
|
class SpaceInvitation(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
token_hash = models.CharField(max_length=64, unique=True)
|
|
parent_space = models.ForeignKey(DriveSpace, on_delete=models.CASCADE, related_name='invitations')
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
|
can_view_parent = models.BooleanField(default=True)
|
|
can_write_parent = models.BooleanField(default=False)
|
|
can_invite = models.BooleanField(default=False)
|
|
quota_bytes = models.BigIntegerField(null=True, blank=True, default=None)
|
|
permissions = models.JSONField(null=True, blank=True, default=None)
|
|
expires_at = models.DateTimeField()
|
|
used_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name='redeemed_invitations')
|
|
used_at = models.DateTimeField(null=True, blank=True)
|
|
revoked_at = models.DateTimeField(null=True, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
class DriveItem(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='drive_items')
|
|
space = models.ForeignKey(DriveSpace, on_delete=models.PROTECT, related_name='items')
|
|
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='children')
|
|
name = models.CharField(max_length=255)
|
|
is_folder = models.BooleanField(default=False)
|
|
storage_key = models.CharField(max_length=120, blank=True)
|
|
# Existing files keep a blank storage_location (legacy /files/ab/<uuid>).
|
|
# Files go under /storage/0001/.../ab/<uuid>, without a /root layer.
|
|
storage_location = models.CharField(max_length=1200, blank=True)
|
|
size = models.BigIntegerField(default=0)
|
|
mime_type = models.CharField(max_length=180, default='application/octet-stream')
|
|
is_shared = models.BooleanField(default=False) # legacy; no global sharing in hierarchical mode
|
|
is_starred = models.BooleanField(default=False)
|
|
trashed_at = models.DateTimeField(null=True, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['-is_folder', 'name', 'id']
|
|
indexes = [models.Index(fields=['owner','parent']), models.Index(fields=['space','trashed_at'])]
|
|
|
|
def clean(self):
|
|
if not self.name or self.name in {'.', '..', '.gooddrive-trash'} or any(c in self.name for c in '/\\\x00\r\n'):
|
|
raise ValidationError('Nom de fichier ou dossier invalide.')
|
|
if len(self.name) > 255:
|
|
raise ValidationError('Nom trop long.')
|
|
if self.parent_id and not self.parent.is_folder:
|
|
raise ValidationError('Le parent doit être un dossier.')
|
|
if self.parent_id and self.parent.space_id != self.space_id:
|
|
raise ValidationError('Le dossier de destination appartient à un autre espace.')
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class UploadSession(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
space = models.ForeignKey(DriveSpace, on_delete=models.CASCADE)
|
|
parent = models.ForeignKey(DriveItem, null=True, blank=True, on_delete=models.SET_NULL)
|
|
name = models.CharField(max_length=255)
|
|
total_size = models.BigIntegerField()
|
|
received = models.BigIntegerField(default=0)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
|
class ItemGrant(models.Model):
|
|
"""Read-only item/folder access, independent of membership in its space."""
|
|
item = models.ForeignKey(DriveItem, on_delete=models.CASCADE, related_name='user_shares')
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='shared_drive_items')
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='+')
|
|
permissions = models.JSONField(null=True, blank=True, default=None)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
constraints = [models.UniqueConstraint(fields=['item', 'user'], name='drive_item_user_share_unique')]
|
|
|
|
|
|
class PublicItemLink(models.Model):
|
|
"""Secret-token link to a single file or directory tree."""
|
|
item = models.ForeignKey(DriveItem, on_delete=models.CASCADE, related_name='public_links')
|
|
token_hash = models.CharField(max_length=64, unique=True)
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='+')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
revoked_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
|
class ArchivedDriveAccount(models.Model):
|
|
"""Inactive, anonymized historical owner of shared files after account removal."""
|
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='drive_archive')
|
|
deleted_at = models.DateTimeField(auto_now_add=True)
|