add v2 specs

This commit is contained in:
m
2026-07-28 09:37:05 +02:00
parent cae6c4d72d
commit 7713835739
+855
View File
@@ -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