"""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) 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) 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) 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/). # New files go under /files/root/0001/.../ab/. 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 {'.', '..'} 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)