add v2 specs
This commit is contained in:
@@ -0,0 +1,855 @@
|
|||||||
|
Architecture Offline-First Multi-Device pour Application Mobile
|
||||||
|
Version : 1.0
|
||||||
|
|
||||||
|
Date : 28 juillet 2026
|
||||||
|
|
||||||
|
Statut : Proposition technique
|
||||||
|
|
||||||
|
1. Introduction et Objectifs
|
||||||
|
Vision
|
||||||
|
Application mobile permettant une lecture complète des ressources sans connexion backend. L'utilisateur peut accéder à ses photos, vidéos, documents et autres fichiers même en mode offline total. Le serveur cloud est un "device" parmi d'autres, permettant une synchronisation bidirectionnelle et une gestion intelligente de l'espace de stockage.
|
||||||
|
Objectifs opérationnels
|
||||||
|
Objectif Description
|
||||||
|
Offline complet Lecture des ressources locales sans aucune dépendance au réseau
|
||||||
|
Libération d'espace Possibilité de ne posséder localement que les ressources récentes (ex: 3 derniers mois)
|
||||||
|
Sync multi-device Synchronisation entre appareils mobiles, desktop et serveur cloud
|
||||||
|
Sécurité 3-2-1 Règle anti-perte : 3 copies, 2 supports différents, 1 hors-site
|
||||||
|
Reprise après crash Reprise des sync interrompues au redémarrage de l'app
|
||||||
|
|
||||||
|
|
||||||
|
2. Principes Fondamentaux
|
||||||
|
2.1 Séparation ressource / localisation
|
||||||
|
Une ressource (photo, PDF, vidéo...) est une entité abstraite avec :
|
||||||
|
· Identité unique (id)
|
||||||
|
· Métadonnées (nom, taille, hash, dates...)
|
||||||
|
· Propriétaire
|
||||||
|
Sa localisation physique est un concept séparé : sur quel device (device A, device B, serveur...) existe une copie réelle du fichier.
|
||||||
|
Ressource (métadonnée) Localisations physiques
|
||||||
|
┌─────────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│ id: abc-123 │◄───────────│ iPhone │ │ Desktop │ │ Cloud │
|
||||||
|
│ name: photo.jpg │ │ ✓ local │ │ ✓ local │ │ ✓ cloud │
|
||||||
|
│ owner: user_42 │ └──────────┘ └──────────┘ └──────────┘
|
||||||
|
└─────────────────┘
|
||||||
|
|
||||||
|
2.2 Le serveur est un "device" comme les autres
|
||||||
|
Tous les emplacements de stockage (mobile, desktop, serveur cloud, backup) sont modélisés dans une table unique storage_locations. Cela unifie la logique de synchronisation.
|
||||||
|
2.3 Un backup est un storage_location de type backup
|
||||||
|
· role = 'backup' : emplacement passif, write-only, jamais source de vérité
|
||||||
|
· Compté dans la règle 3-2-1 pour la sécurité anti-purge
|
||||||
|
· Ne peut pas être supprimé par les règles de rétention standard
|
||||||
|
|
||||||
|
3. Modèle de Données SQL (PostgreSQL)
|
||||||
|
3.1 Table users — Utilisateurs et groupes
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
display_name VARCHAR(255) NOT NULL,
|
||||||
|
parent_user_id UUID REFERENCES users(id), -- pour groupes/organisations
|
||||||
|
storage_quota_bytes BIGINT DEFAULT 10737418240, -- 10 Go par défaut
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour recherche par email et hiérarchie
|
||||||
|
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_users_parent ON users(parent_user_id) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
3.2 Table resources — Métadonnées universelles des ressources
|
||||||
|
CREATE TABLE resources (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('file', 'folder')),
|
||||||
|
|
||||||
|
-- Hiérarchie (type folder peut contenir des resources)
|
||||||
|
name VARCHAR(1024) NOT NULL,
|
||||||
|
parent_id UUID REFERENCES resources(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Propriété et droits
|
||||||
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Caractéristiques techniques
|
||||||
|
content_hash VARCHAR(128), -- SHA-256 pour déduplication
|
||||||
|
size_bytes BIGINT DEFAULT 0,
|
||||||
|
mime_type VARCHAR(255),
|
||||||
|
category VARCHAR(50) CHECK (category IN (
|
||||||
|
'photo', 'video', 'document', 'audio', 'other'
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Dates métier
|
||||||
|
taken_at TIMESTAMPTZ, -- date de prise de vue (EXIF pour photos)
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ, -- soft delete
|
||||||
|
|
||||||
|
-- Contrôle
|
||||||
|
CONSTRAINT unique_name_per_parent UNIQUE (parent_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour requêtes fréquentes
|
||||||
|
CREATE INDEX idx_resources_owner ON resources(owner_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_resources_category ON resources(category) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_resources_taken_at ON resources(taken_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_resources_content_hash ON resources(content_hash) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_resources_parent ON resources(parent_id);
|
||||||
|
|
||||||
|
3.3 Table resource_variants — Variantes (thumbnails, previews)
|
||||||
|
CREATE TABLE resource_variants (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Type de variant
|
||||||
|
variant_type VARCHAR(50) NOT NULL CHECK (variant_type IN (
|
||||||
|
'thumbnail_small', -- 150px
|
||||||
|
'thumbnail_medium', -- 300px
|
||||||
|
'thumbnail_large', -- 600px
|
||||||
|
'preview', -- version optimisée pour affichage rapide
|
||||||
|
'transcoded_video', -- version mp4 pour streaming
|
||||||
|
'pdf_preview' -- page 1 en image
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Métadonnées du variant
|
||||||
|
size_bytes BIGINT NOT NULL,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
content_hash VARCHAR(128),
|
||||||
|
mime_type VARCHAR(255) DEFAULT 'image/jpeg',
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_variants_resource ON resource_variants(resource_id);
|
||||||
|
CREATE UNIQUE INDEX idx_variants_resource_type ON resource_variants(resource_id, variant_type);
|
||||||
|
|
||||||
|
3.4 Table storage_locations — Emplacements de stockage
|
||||||
|
CREATE TABLE storage_locations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Type physique
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN (
|
||||||
|
'mobile', -- smartphone/tablette
|
||||||
|
'desktop', -- ordinateur
|
||||||
|
'server', -- serveur/cloud (le "cloud" est un device comme les autres)
|
||||||
|
'backup' -- serveur de backup
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Identité de l'emplacement
|
||||||
|
name VARCHAR(255) NOT NULL, -- ex: "iPhone de Jean", "Cloud principal"
|
||||||
|
device_id VARCHAR(255) UNIQUE, -- identifiant unique du device
|
||||||
|
|
||||||
|
-- Rôle dans la stratégie 3-2-1
|
||||||
|
role VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (role IN (
|
||||||
|
'active', -- emplacement principal, peut être source de sync
|
||||||
|
'backup' -- backup passif, write-only, compte pour sécurité
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Durabilité du stockage (important pour règle 3-2-1)
|
||||||
|
is_durable BOOLEAN DEFAULT true, -- false = stockage éphémère (RAM, cache)
|
||||||
|
|
||||||
|
-- Espace
|
||||||
|
total_bytes BIGINT,
|
||||||
|
used_bytes BIGINT DEFAULT 0,
|
||||||
|
|
||||||
|
-- Statut
|
||||||
|
is_online BOOLEAN DEFAULT false, -- le device est-il joignable ?
|
||||||
|
last_seen_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_locations_user ON storage_locations(user_id);
|
||||||
|
CREATE INDEX idx_locations_type ON storage_locations(type);
|
||||||
|
CREATE INDEX idx_locations_role ON storage_locations(role);
|
||||||
|
|
||||||
|
3.5 Table resource_placements — Pivot ressource / localisation avec statut
|
||||||
|
CREATE TABLE resource_placements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
||||||
|
location_id UUID NOT NULL REFERENCES storage_locations(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Statut de présence locale
|
||||||
|
-- local_only : existe uniquement ici, pas encore synchronisé
|
||||||
|
-- synced : synchronisé, existe ici et sur le serveur cloud
|
||||||
|
-- cloud_only : n'existe pas ici, uniquement sur le cloud
|
||||||
|
-- pending_download : téléchargement en cours (intention matérialisée)
|
||||||
|
-- pending_upload : upload en cours
|
||||||
|
status VARCHAR(30) NOT NULL DEFAULT 'synced' CHECK (status IN (
|
||||||
|
'local_only', 'synced', 'cloud_only', 'pending_download', 'pending_upload'
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Chemin local si présent
|
||||||
|
local_path VARCHAR(4096),
|
||||||
|
|
||||||
|
-- Timestamps de sync
|
||||||
|
downloaded_at TIMESTAMPTZ, -- quand la copie locale a été faite
|
||||||
|
uploaded_at TIMESTAMPTZ, -- quand upload vers cloud terminé
|
||||||
|
verified_at TIMESTAMPTZ, -- dernière vérification intégrité
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_resource_location UNIQUE (resource_id, location_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_placements_resource ON resource_placements(resource_id);
|
||||||
|
CREATE INDEX idx_placements_location ON resource_placements(location_id);
|
||||||
|
CREATE INDEX idx_placements_status ON resource_placements(status);
|
||||||
|
CREATE INDEX idx_placements_location_status ON resource_placements(location_id, status);
|
||||||
|
|
||||||
|
-- Vue materialisée pour stats par location
|
||||||
|
CREATE MATERIALIZED VIEW mv_location_stats AS
|
||||||
|
SELECT
|
||||||
|
location_id,
|
||||||
|
COUNT(*) FILTER (WHERE status IN ('local_only', 'synced')) as local_count,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'cloud_only') as cloud_only_count,
|
||||||
|
SUM(size_bytes) FILTER (WHERE status IN ('local_only', 'synced')) as local_bytes
|
||||||
|
FROM resource_placements
|
||||||
|
JOIN resources ON resources.id = resource_placements.resource_id
|
||||||
|
GROUP BY location_id;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_mv_location_stats ON mv_location_stats(location_id);
|
||||||
|
|
||||||
|
3.6 Table retention_policies — Politiques de rétention déclaratives
|
||||||
|
CREATE TABLE retention_policies (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Identifiant de la policy
|
||||||
|
name VARCHAR(255) NOT NULL, -- ex: "3 derniers mois sur mobile"
|
||||||
|
|
||||||
|
-- Cible : quel type de ressource et catégorie
|
||||||
|
target_type VARCHAR(20) CHECK (target_type IN ('file', 'folder', 'all')),
|
||||||
|
target_category VARCHAR(50) CHECK (target_category IN (
|
||||||
|
'photo', 'video', 'document', 'audio', 'other', 'all'
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Règle de rétention (expressions simples, user-friendly)
|
||||||
|
-- Exemples :
|
||||||
|
-- 'all' → tout garder localement
|
||||||
|
-- 'last_months:3' → garder uniquement les 3 derniers mois
|
||||||
|
-- 'favorites_only' → garder uniquement les favoris
|
||||||
|
-- 'last_months:6,category:photo' → 6 derniers mois pour photos
|
||||||
|
retention_rule VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
-- Location cible (null = toutes les locations actives)
|
||||||
|
target_location_id UUID REFERENCES storage_locations(id),
|
||||||
|
|
||||||
|
-- actif ?
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_policies_user ON retention_policies(user_id);
|
||||||
|
CREATE INDEX idx_policies_active ON retention_policies(is_active) WHERE is_active = true;
|
||||||
|
|
||||||
|
3.7 Table sync_queue — File de synchronisation
|
||||||
|
CREATE TABLE sync_queue (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
||||||
|
location_id UUID NOT NULL REFERENCES storage_locations(id),
|
||||||
|
|
||||||
|
-- Type d'opération
|
||||||
|
operation VARCHAR(30) NOT NULL CHECK (operation IN (
|
||||||
|
'upload', --上传到云端
|
||||||
|
'download', --从云端下载
|
||||||
|
'delete', --删除本地副本
|
||||||
|
'metadata_sync' --同步元数据
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Statut
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN (
|
||||||
|
'pending', -- en attente
|
||||||
|
'in_progress', -- en cours
|
||||||
|
'completed', -- terminé
|
||||||
|
'failed', -- échoué
|
||||||
|
'cancelled' -- annulé
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Priorité (0 =最高, 100 =最低) - pour ordonner les opérations
|
||||||
|
priority INTEGER DEFAULT 50,
|
||||||
|
|
||||||
|
-- Gestion des erreurs
|
||||||
|
retry_count INTEGER DEFAULT 0,
|
||||||
|
max_retries INTEGER DEFAULT 3,
|
||||||
|
error_message TEXT,
|
||||||
|
last_error_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
started_at TIMESTAMPTZ,
|
||||||
|
completed_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_queue_status ON sync_queue(status) WHERE status IN ('pending', 'in_progress');
|
||||||
|
CREATE INDEX idx_queue_resource ON sync_queue(resource_id);
|
||||||
|
CREATE INDEX idx_queue_priority ON sync_queue(priority, created_at);
|
||||||
|
CREATE INDEX idx_queue_pending_downloads ON sync_queue(location_id, operation, status)
|
||||||
|
WHERE operation = 'download' AND status = 'pending';
|
||||||
|
CREATE INDEX idx_queue_pending_uploads ON sync_queue(location_id, operation, status)
|
||||||
|
WHERE operation = 'upload' AND status = 'pending';
|
||||||
|
|
||||||
|
3.8 Tables ReBAC — Relations et droits (Relation-Based Access Control)
|
||||||
|
-- Table des relations entre utilisateurs et entités
|
||||||
|
CREATE TABLE entity_relations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Sujet de la relation (qui)
|
||||||
|
subject_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
subject_type VARCHAR(20) NOT NULL DEFAULT 'user' CHECK (subject_type IN ('user', 'group')),
|
||||||
|
|
||||||
|
-- Type de relation
|
||||||
|
relation VARCHAR(50) NOT NULL CHECK (relation IN (
|
||||||
|
'owner', -- propriétaire (CRUD total)
|
||||||
|
'editor', -- peut modifier
|
||||||
|
'viewer', -- lecture seule
|
||||||
|
'commenter', -- peut commenter
|
||||||
|
'uploader', -- peut ajouter des fichiers
|
||||||
|
'member', -- membre d'un groupe
|
||||||
|
'parent', -- parent organization
|
||||||
|
'child' -- enfant (sub-organisation)
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Entité cible (sur quoi)
|
||||||
|
entity_type VARCHAR(50) NOT NULL CHECK (entity_type IN (
|
||||||
|
'user', 'group', 'resource', 'folder', 'collection'
|
||||||
|
)),
|
||||||
|
entity_id UUID NOT NULL,
|
||||||
|
|
||||||
|
-- Héritage (cette relation se propage-t-elle aux enfants ?)
|
||||||
|
inherit BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_subject_entity_relation UNIQUE (subject_id, entity_id, relation)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_relations_subject ON entity_relations(subject_id);
|
||||||
|
CREATE INDEX idx_relations_entity ON entity_relations(entity_type, entity_id);
|
||||||
|
CREATE INDEX idx_relations_type ON entity_relations(relation);
|
||||||
|
|
||||||
|
-- Vue pour résolution rapide des droits sur ressources
|
||||||
|
CREATE VIEW resource_permissions_view AS
|
||||||
|
SELECT
|
||||||
|
er.subject_id,
|
||||||
|
er.entity_id AS resource_id,
|
||||||
|
er.relation,
|
||||||
|
r.owner_id,
|
||||||
|
r.parent_id,
|
||||||
|
er.inherit
|
||||||
|
FROM entity_relations er
|
||||||
|
JOIN resources r ON r.id = er.entity_id
|
||||||
|
WHERE er.entity_type = 'resource';
|
||||||
|
|
||||||
|
|
||||||
|
4. Fonctions et Requêtes SQL Clés
|
||||||
|
4.1 Déterminer le statut d'affichage d'une ressource sur un device
|
||||||
|
-- Retourne le statut visuel d'une ressource pour un device donné
|
||||||
|
-- Utile pour l'UI : badge cloud, spinner téléchargement, miniature locale...
|
||||||
|
CREATE OR REPLACE FUNCTION get_resource_display_status(
|
||||||
|
p_resource_id UUID,
|
||||||
|
p_location_id UUID
|
||||||
|
) RETURNS VARCHAR(30) AS $$
|
||||||
|
DECLARE
|
||||||
|
v_placement record;
|
||||||
|
v_resource record;
|
||||||
|
BEGIN
|
||||||
|
-- Récupérer la ressource
|
||||||
|
SELECT * INTO v_resource FROM resources WHERE id = p_resource_id;
|
||||||
|
IF v_resource IS NULL THEN
|
||||||
|
RETURN 'not_found';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Vérifier si la ressource est dans la corbeille
|
||||||
|
IF v_resource.deleted_at IS NOT NULL THEN
|
||||||
|
RETURN 'deleted';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Récupérer le placement sur ce device
|
||||||
|
SELECT * INTO v_placement
|
||||||
|
FROM resource_placements
|
||||||
|
WHERE resource_id = p_resource_id AND location_id = p_location_id;
|
||||||
|
|
||||||
|
IF v_placement IS NULL THEN
|
||||||
|
-- Pas de placement = pas sur ce device, chercher sur cloud
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM resource_placements
|
||||||
|
WHERE resource_id = p_resource_id
|
||||||
|
AND status IN ('synced', 'cloud_only')
|
||||||
|
AND location_id IN (
|
||||||
|
SELECT id FROM storage_locations WHERE type = 'server'
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
RETURN 'cloud_available'; -- existe sur cloud, peut être téléchargé
|
||||||
|
END IF;
|
||||||
|
RETURN 'not_available';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Retourner le statut du placement
|
||||||
|
RETURN v_placement.status;
|
||||||
|
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Exemple d'utilisation dans une requête
|
||||||
|
/*
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.name,
|
||||||
|
r.category,
|
||||||
|
get_resource_display_status(r.id, 'device-uuid-here') as display_status
|
||||||
|
FROM resources r
|
||||||
|
WHERE r.owner_id = 'user-uuid'
|
||||||
|
ORDER BY r.taken_at DESC;
|
||||||
|
*/
|
||||||
|
|
||||||
|
4.2 Calculer l'espace utilisé par un storage_location
|
||||||
|
-- Calcule l'espace disque utilisé par un emplacement
|
||||||
|
CREATE OR REPLACE FUNCTION calculate_location_used_bytes(
|
||||||
|
p_location_id UUID
|
||||||
|
) RETURNS BIGINT AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN COALESCE(
|
||||||
|
(
|
||||||
|
SELECT SUM(r.size_bytes)
|
||||||
|
FROM resource_placements rp
|
||||||
|
JOIN resources r ON r.id = rp.resource_id
|
||||||
|
WHERE rp.location_id = p_location_id
|
||||||
|
AND rp.status IN ('local_only', 'synced')
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
), 0
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Mise à jour automatique de used_bytes dans storage_locations
|
||||||
|
CREATE OR REPLACE FUNCTION update_location_used_bytes()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE storage_locations
|
||||||
|
SET used_bytes = calculate_location_used_bytes(NEW.location_id)
|
||||||
|
WHERE id = NEW.location_id;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_update_location_bytes
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON resource_placements
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_location_used_bytes();
|
||||||
|
|
||||||
|
4.3 Appliquer une politique de rétention (proposer purge avec vérification 3-2-1)
|
||||||
|
-- Identifie les ressources candidates à la purge locale
|
||||||
|
-- Applique la règle 3-2-1 : ne purger que s'il existe >= 2 copies durables ailleurs
|
||||||
|
CREATE OR REPLACE FUNCTION get_resources_for_purge(
|
||||||
|
p_location_id UUID,
|
||||||
|
p_retention_rule VARCHAR(255),
|
||||||
|
p_target_category VARCHAR(50) DEFAULT 'all'
|
||||||
|
) RETURNS TABLE(
|
||||||
|
resource_id UUID,
|
||||||
|
resource_name VARCHAR(1024),
|
||||||
|
size_bytes BIGINT,
|
||||||
|
current_status VARCHAR(30),
|
||||||
|
can_purge BOOLEAN,
|
||||||
|
copies_elsewhere INTEGER,
|
||||||
|
durable_copies_elsewhere INTEGER
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
WITH
|
||||||
|
-- Parser la règle de rétention (ex: 'last_months:3')
|
||||||
|
retention_params AS (
|
||||||
|
SELECT
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(SPLIT_PART(p_retention_rule, ':', 2), 'all'),
|
||||||
|
'9999'
|
||||||
|
)::INTEGER as months,
|
||||||
|
p_retention_rule as raw_rule
|
||||||
|
),
|
||||||
|
-- Ressources sur ce device qui ne sont pas 'cloud_only' (donc occupent de l'espace)
|
||||||
|
local_resources AS (
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.name,
|
||||||
|
r.size_bytes,
|
||||||
|
r.category,
|
||||||
|
r.taken_at,
|
||||||
|
rp.status
|
||||||
|
FROM resources r
|
||||||
|
JOIN resource_placements rp ON rp.resource_id = r.id
|
||||||
|
WHERE rp.location_id = p_location_id
|
||||||
|
AND rp.status IN ('local_only', 'synced')
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
AND (p_target_category = 'all' OR r.category = p_target_category)
|
||||||
|
),
|
||||||
|
-- Ressources à purger selon la politique de rétention
|
||||||
|
candidates AS (
|
||||||
|
SELECT lr.*
|
||||||
|
FROM local_resources lr
|
||||||
|
CROSS JOIN retention_params rp
|
||||||
|
WHERE
|
||||||
|
-- Si 'all', garder tout
|
||||||
|
rp.raw_rule != 'all'
|
||||||
|
AND
|
||||||
|
-- Sinon, vérifier l'ancienneté
|
||||||
|
(
|
||||||
|
lr.taken_at IS NULL
|
||||||
|
OR lr.taken_at < NOW() - (rp.months || ' months')::INTERVAL
|
||||||
|
)
|
||||||
|
),
|
||||||
|
-- Compter les copies ailleurs pour chaque candidat
|
||||||
|
copies_count AS (
|
||||||
|
SELECT
|
||||||
|
c.id,
|
||||||
|
COUNT(DISTINCT rp.location_id) as total_copies,
|
||||||
|
COUNT(DISTINCT rp.location_id) FILTER (
|
||||||
|
WHERE sl.is_durable = true AND sl.role = 'active'
|
||||||
|
) as durable_active_copies,
|
||||||
|
COUNT(DISTINCT rp.location_id) FILTER (
|
||||||
|
WHERE sl.is_durable = true AND sl.role = 'backup'
|
||||||
|
) as backup_copies
|
||||||
|
FROM candidates c
|
||||||
|
JOIN resource_placements rp ON rp.resource_id = c.id
|
||||||
|
JOIN storage_locations sl ON sl.id = rp.location_id
|
||||||
|
WHERE rp.location_id != p_location_id
|
||||||
|
AND rp.status IN ('synced', 'cloud_only')
|
||||||
|
GROUP BY c.id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
c.id as resource_id,
|
||||||
|
c.name as resource_name,
|
||||||
|
c.size_bytes,
|
||||||
|
c.status as current_status,
|
||||||
|
-- Règle 3-2-1 : on purge si >= 2 copies durables elsewhere
|
||||||
|
CASE
|
||||||
|
WHEN cc.total_copies >= 2 THEN true
|
||||||
|
WHEN cc.durable_active_copies + cc.backup_copies >= 1 AND cc.total_copies >= 2 THEN true
|
||||||
|
ELSE false
|
||||||
|
END as can_purge,
|
||||||
|
COALESCE(cc.total_copies, 0) as copies_elsewhere,
|
||||||
|
COALESCE(cc.durable_active_copies + cc.backup_copies, 0) as durable_copies_elsewhere
|
||||||
|
FROM candidates c
|
||||||
|
LEFT JOIN copies_count cc ON cc.id = c.id
|
||||||
|
ORDER BY c.taken_at ASC NULLS LAST; -- les plus anciennes en premier
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Exemple d'utilisation : identifier ce qui peut être purgé
|
||||||
|
/*
|
||||||
|
SELECT * FROM get_resources_for_purge(
|
||||||
|
'iphone-location-uuid',
|
||||||
|
'last_months:3',
|
||||||
|
'photo'
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
4.4 Récupérer les sync interrompues à reprendre au démarrage
|
||||||
|
-- Téléchargements en attente ou en cours à reprendre
|
||||||
|
CREATE OR REPLACE FUNCTION get_pending_downloads(p_location_id UUID)
|
||||||
|
RETURNS TABLE(
|
||||||
|
queue_id UUID,
|
||||||
|
resource_id UUID,
|
||||||
|
resource_name VARCHAR(1024),
|
||||||
|
size_bytes BIGINT,
|
||||||
|
retry_count INTEGER,
|
||||||
|
error_message TEXT
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
q.id as queue_id,
|
||||||
|
q.resource_id,
|
||||||
|
r.name,
|
||||||
|
r.size_bytes,
|
||||||
|
q.retry_count,
|
||||||
|
q.error_message
|
||||||
|
FROM sync_queue q
|
||||||
|
JOIN resources r ON r.id = q.resource_id
|
||||||
|
WHERE q.location_id = p_location_id
|
||||||
|
AND q.operation = 'download'
|
||||||
|
AND q.status IN ('pending', 'in_progress', 'failed')
|
||||||
|
AND q.retry_count < q.max_retries
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
ORDER BY q.priority ASC, q.created_at ASC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Uploads en attente ou en cours à reprendre
|
||||||
|
CREATE OR REPLACE FUNCTION get_pending_uploads(p_location_id UUID)
|
||||||
|
RETURNS TABLE(
|
||||||
|
queue_id UUID,
|
||||||
|
resource_id UUID,
|
||||||
|
resource_name VARCHAR(1024),
|
||||||
|
size_bytes BIGINT,
|
||||||
|
status VARCHAR(20),
|
||||||
|
retry_count INTEGER
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
q.id as queue_id,
|
||||||
|
q.resource_id,
|
||||||
|
r.name,
|
||||||
|
r.size_bytes,
|
||||||
|
q.status,
|
||||||
|
q.retry_count
|
||||||
|
FROM sync_queue q
|
||||||
|
JOIN resources r ON r.id = q.resource_id
|
||||||
|
WHERE q.location_id = p_location_id
|
||||||
|
AND q.operation = 'upload'
|
||||||
|
AND q.status IN ('pending', 'in_progress', 'failed')
|
||||||
|
AND q.retry_count < q.max_retries
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
ORDER BY q.priority ASC, q.created_at ASC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Placements en 'pending_download' à finaliser
|
||||||
|
CREATE OR REPLACE FUNCTION get_pending_download_placements(p_location_id UUID)
|
||||||
|
RETURNS TABLE(
|
||||||
|
placement_id UUID,
|
||||||
|
resource_id UUID,
|
||||||
|
resource_name VARCHAR(1024)
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
rp.id as placement_id,
|
||||||
|
rp.resource_id,
|
||||||
|
r.name
|
||||||
|
FROM resource_placements rp
|
||||||
|
JOIN resources r ON r.id = rp.resource_id
|
||||||
|
WHERE rp.location_id = p_location_id
|
||||||
|
AND rp.status = 'pending_download'
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
ORDER BY rp.updated_at ASC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
4.5 Requête CTE récursive pour résolution des droits ReBAC
|
||||||
|
-- Résout tous les droits d'un utilisateur sur une ressource (et ses parents)
|
||||||
|
-- Prend en compte :
|
||||||
|
-- - Droits directs sur la ressource
|
||||||
|
-- - Héritage depuis le parent (folder)
|
||||||
|
-- - Appartenance à des groupes (parent_user_id)
|
||||||
|
-- - Permissions inherited depuis les organisations parentes
|
||||||
|
CREATE OR REPLACE FUNCTION resolve_user_resource_permissions(
|
||||||
|
p_user_id UUID,
|
||||||
|
p_resource_id UUID
|
||||||
|
) RETURNS TABLE(relation VARCHAR(50), entity_id UUID, inherit BOOLEAN, source_type VARCHAR(20)) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
WITH RECURSIVE
|
||||||
|
-- 1. Ressource et sa hiérarchie de parents
|
||||||
|
resource_hierarchy AS (
|
||||||
|
-- Starting point
|
||||||
|
SELECT r.id, r.parent_id, r.owner_id, 0 as depth
|
||||||
|
FROM resources r
|
||||||
|
WHERE r.id = p_resource_id
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- Parents récursifs
|
||||||
|
SELECT r.id, r.parent_id, r.owner_id, rh.depth + 1
|
||||||
|
FROM resources r
|
||||||
|
JOIN resource_hierarchy rh ON r.id = rh.parent_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 2. Hiérarchie de groupes de l'utilisateur (via parent_user_id)
|
||||||
|
user_groups AS (
|
||||||
|
SELECT id, parent_user_id, 0 as depth
|
||||||
|
FROM users
|
||||||
|
WHERE id = p_user_id
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT u.id, u.parent_user_id, ug.depth + 1
|
||||||
|
FROM users u
|
||||||
|
JOIN user_groups ug ON u.parent_user_id = ug.id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 3. Droits directs sur la ressource
|
||||||
|
direct_rights AS (
|
||||||
|
SELECT er.relation, er.entity_id, er.inherit, 'direct' as source_type
|
||||||
|
FROM entity_relations er
|
||||||
|
WHERE er.subject_id = p_user_id
|
||||||
|
AND er.entity_type = 'resource'
|
||||||
|
AND er.entity_id = p_resource_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 4. Droits hérités depuis le parent folder
|
||||||
|
inherited_rights AS (
|
||||||
|
SELECT
|
||||||
|
er.relation,
|
||||||
|
er.entity_id,
|
||||||
|
true as inherit, -- forcé à true pour héritage
|
||||||
|
'inherited' as source_type
|
||||||
|
FROM entity_relations er
|
||||||
|
JOIN resource_hierarchy rh ON er.entity_id = rh.id
|
||||||
|
WHERE er.subject_id = p_user_id
|
||||||
|
AND er.entity_type = 'resource'
|
||||||
|
AND er.inherit = true
|
||||||
|
AND rh.depth > 0 -- exclude self
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 5. Droits via appartenance à un groupe
|
||||||
|
group_rights AS (
|
||||||
|
SELECT er.relation, er.entity_id, er.inherit, 'group' as source_type
|
||||||
|
FROM entity_relations er
|
||||||
|
JOIN user_groups ug ON er.subject_id = ug.id
|
||||||
|
WHERE ug.depth > 0 -- exclude self (already in direct_rights)
|
||||||
|
AND er.entity_type = 'resource'
|
||||||
|
AND er.entity_id = p_resource_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 6. Droits owner (tout utilisateur est owner de ses ressources)
|
||||||
|
owner_rights AS (
|
||||||
|
SELECT 'owner' as relation, id as entity_id, false as inherit, 'owner' as source_type
|
||||||
|
FROM resource_hierarchy
|
||||||
|
WHERE owner_id = p_user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Combinaison de tous les droits
|
||||||
|
SELECT relation, entity_id, inherit, source_type
|
||||||
|
FROM direct_rights
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT relation, entity_id, inherit, source_type
|
||||||
|
FROM inherited_rights
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT relation, entity_id, inherit, source_type
|
||||||
|
FROM group_rights
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT relation, entity_id, inherit, source_type
|
||||||
|
FROM owner_rights;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Vue simplifiée pour vérifier rapidement si un user peut voir une ressource
|
||||||
|
CREATE OR REPLACE FUNCTION can_user_access_resource(
|
||||||
|
p_user_id UUID,
|
||||||
|
p_resource_id UUID,
|
||||||
|
p_required_relation VARCHAR(50) DEFAULT 'viewer'
|
||||||
|
) RETURNS BOOLEAN AS $$
|
||||||
|
DECLARE
|
||||||
|
v_has_access BOOLEAN;
|
||||||
|
BEGIN
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM resolve_user_resource_permissions(p_user_id, p_resource_id) perms
|
||||||
|
WHERE perms.relation IN (
|
||||||
|
CASE p_required_relation
|
||||||
|
WHEN 'viewer' THEN ARRAY['owner', 'viewer', 'editor', 'commenter']
|
||||||
|
WHEN 'editor' THEN ARRAY['owner', 'editor']
|
||||||
|
WHEN 'owner' THEN ARRAY['owner']
|
||||||
|
ELSE ARRAY[p_required_relation]
|
||||||
|
END
|
||||||
|
)
|
||||||
|
) INTO v_has_access;
|
||||||
|
|
||||||
|
RETURN COALESCE(v_has_access, false);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Exemple : vérifier si user peut voir une photo
|
||||||
|
/*
|
||||||
|
SELECT can_user_access_resource('user-uuid', 'resource-uuid', 'viewer');
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
5. Features et Comportements Utilisateur
|
||||||
|
5.1 Mode Offline Complet
|
||||||
|
· ✅ Lecture des ressources locales sans aucune requête réseau
|
||||||
|
· ✅ Navigation dans les dossiers avec métadonnées en local
|
||||||
|
· ✅ Affichage des miniatures pré-générées et en cache
|
||||||
|
· ✅ Lecture des fichiers (documents, photos, vidéos) depuis le stockage local
|
||||||
|
· ⚠️ Limitation : création/modification de contenu nécessite une sync ultérieure
|
||||||
|
5.2 Gestion Intelligente de l'Espace
|
||||||
|
· 📱 Politique de rétention configurable par l'utilisateur
|
||||||
|
o "Garder les 3 derniers mois sur mon téléphone"
|
||||||
|
o "Garder uniquement les favoris"
|
||||||
|
o "Tout synchroniser" (mode desktop)
|
||||||
|
· 🗑️ Purge automatique suggérée avec validation 3-2-1
|
||||||
|
o Ne supprime jamais une ressource qui n'aurait que 1 copie ailleurs
|
||||||
|
o Interface claire : "X Go peuvent être libérés"
|
||||||
|
· 📊 Indicateur d'espace utilisé en temps réel par catégorie
|
||||||
|
5.3 Synchronisation Multi-Device
|
||||||
|
· 🔄 Sync bidirectionnelle : mobile ↔ cloud ↔ desktop
|
||||||
|
· ⏸️ Reprise après interruption : au démarrage, reprend les uploads/downloads échoués
|
||||||
|
· 🎯 Priorisation : les ressources récemment consultées sont synchronisées en priorité
|
||||||
|
· 🔍 Détection de conflits : si même fichier modifié sur 2 devices, proposer à l'utilisateur
|
||||||
|
5.4 Sécurité Anti-Perte (Règle 3-2-1)
|
||||||
|
· 📦 3 copies minimum de chaque ressource importante
|
||||||
|
· 💾 2 supports différents (ex: mobile + cloud, ou desktop + backup)
|
||||||
|
· ☁️ 1 copie hors-site (serveur cloud ou backup distant)
|
||||||
|
· 🚫 Protection des backups : les storage_locations avec role='backup' ne sont jamais purgés
|
||||||
|
5.5 Variantes et Optimisation
|
||||||
|
· 🖼️ Thumbnails multi-résolution : 150px, 300px, 600px pour affichage rapide
|
||||||
|
· 🎬 Previews vidéo : version mp4 optimisée pour streaming
|
||||||
|
· 📄 Aperçus PDF : première page en image
|
||||||
|
· 💾 Cache intelligent : les variantes sont prioritaires pour la sync
|
||||||
|
|
||||||
|
6. Schéma Récapitulatif des Tables
|
||||||
|
Table Rôle Clé Primaire Liens Clés
|
||||||
|
users Utilisateurs et groupes (orgs) id parent_user_id (self-ref)
|
||||||
|
resources Métadonnées universelles id owner_id, parent_id
|
||||||
|
resource_variants Thumbnails, previews id resource_id
|
||||||
|
storage_locations Emplacements (devices, cloud, backup) id user_id
|
||||||
|
resource_placements Pivot avec statut de présence id resource_id, location_id
|
||||||
|
retention_policies Règles de purge user-friendly id user_id, target_location_id
|
||||||
|
sync_queue File de sync en attente id resource_id, location_id
|
||||||
|
entity_relations Droits ReBAC sujet→entité id subject_id, entity_id
|
||||||
|
|
||||||
|
Diagramme de relations simplifié
|
||||||
|
users (1)──┬──(n) resources
|
||||||
|
│
|
||||||
|
└──(n) storage_locations (1)──(n) resource_placements (n)──(1) resources
|
||||||
|
│
|
||||||
|
└──(n) retention_policies
|
||||||
|
│
|
||||||
|
└──(n) entity_relations
|
||||||
|
|
||||||
|
storage_locations ──(1) sync_queue (n)──(1) resources
|
||||||
|
│
|
||||||
|
└──(1) retention_policies (target)
|
||||||
|
|
||||||
|
users (1)──(n) entity_relations (n)──(1) resources
|
||||||
|
│ │
|
||||||
|
└──(self-ref) └──(target: folder, collection, group...)
|
||||||
|
parent_user_id
|
||||||
|
|
||||||
|
|
||||||
|
7. Notes d'Implémentation
|
||||||
|
Ordre de création des tables
|
||||||
|
-- Niveau 1 : tables sans dépendances
|
||||||
|
1. users
|
||||||
|
|
||||||
|
-- Niveau 2 : tables avec FK vers niveau 1
|
||||||
|
2. storage_locations
|
||||||
|
3. resources
|
||||||
|
4. entity_relations
|
||||||
|
|
||||||
|
-- Niveau 3 : tables avec FK vers niveaux 1-2
|
||||||
|
5. resource_variants
|
||||||
|
6. resource_placements
|
||||||
|
7. retention_policies
|
||||||
|
8. sync_queue
|
||||||
|
|
||||||
|
Considérations de performance
|
||||||
|
· Indexes : créés sur toutes les FK et colonnes filtrées fréquemment
|
||||||
|
· Materialized views : pour les stats d'espace (mv_location_stats)
|
||||||
|
· Soft delete : deleted_at au lieu de suppression physique pour traçabilité
|
||||||
|
· Batch processing : le sync_queue permet de traiter les opérations par lots
|
||||||
|
Points à adresser en conception détaillée
|
||||||
|
1. Conflict resolution : stratégie quand même fichier modifié sur 2 devices
|
||||||
|
2. Bandwidth management : limiter la sync en mode mobile (data economy)
|
||||||
|
3. Encryption at rest : stockage chiffré sur les devices mobiles
|
||||||
|
4. Thumbnail generation : où et quand générer les variantes
|
||||||
|
5. Quota management : tracking et alertes quand接近存储限额
|
||||||
|
|
||||||
|
Document généré le 28 juillet 2026 — Architecture v1.0
|
||||||
Reference in New Issue
Block a user