GStorage — Platform Design
GStorage — platforma PaaS de stocare a ecosistemului gStack. Este stratul generic de infrastructură care stochează fișiere, imagini, text și seturi de date și le expune prin API/SDK (headless, ca GLog) sau printr-un portal de date deschise cu board de administrare (platformă completă, ca interdictii/gdocs).
Acest document este artefactul de design (sursa de adevăr pentru arhitectură).
Modelul de entități trăiește în ../gstorage.jdl; DDL-ul derivat
în db_schema.md; cerințele în functional_alfa.md;
SDK-ul în sdk.md.
Poziționare față de gdocs. GStorage = stratul de infrastructură (object-store generic + catalog de date deschise + SDK). gdocs este o aplicație de domeniu despre documente guvernamentale (semnare MSign, OCR, niveluri L1/L2/L3) care își poate stoca binarele prin GStorage. GStorage nu semnează și nu face OCR — stochează octeți bruți și publică seturi de date.
1. System context
Section titled “1. System context”flowchart TB subgraph Public["Utilizatori & consumatori"] anon["Cetățean ne-autentificat<br/>(căutare/descărcare date publice)"] operator["Operator / Admin / Emitent<br/>(board de administrare)"] svc["Microserviciu / sistem extern<br/>(SDK sau /api/v1)"] ftp["Client FTP/SFTP"] end
subgraph GStorage["GStorage (JHipster microservices)"] gw["gateway :8080<br/>Spring Cloud Gateway + Angular 19 UI<br/>(board + portal public, login dual)"] api["gstorage :8100 (×N replici)<br/>Spring Boot — motor de stocare + /api/v1"] ftpd["Server FTP/SFTP<br/>(Apache MINA, punte spre object-store)"] worker["Workeri async (RabbitMQ)<br/>(Tika, backup, arhivare, finalizare transfer)"] db[("PostgreSQL<br/>metadate + audit")] es[("Elasticsearch<br/>full-text + fațete")] mq[["RabbitMQ"]] store[("Object store<br/>MinIO / S3 — via StorageBackend")] reg{{"JHipster Registry<br/>Eureka + Config :8761"}} end
subgraph Ext["Ecosistem gStack"] kc["Keycloak SSO<br/>(realm gstorage)"] glog["GLog<br/>(audit centralizat)"] gnotify["GNotify<br/>(notificări/alarme)"] consumers["Alte SaaS/PaaS<br/>(gdocs, interdictii, …)"] end
anon -->|HTTPS| gw operator -->|HTTPS| gw svc -->|Bearer / API-key| gw svc -.->|direct, headless| api ftp -->|FTPS/SFTP| ftpd gw -->|/services/gstorage/**| api api --- ftpd api --> db api --> es api --> store api --> mq mq --> worker worker --> store worker --> es worker --> db gw & api -->|register| reg gw -->|OIDC login| kc api -->|resource-server| kc api -->|audit SDK| glog api -->|alarme SDK| gnotify consumers -->|SDK / API| gw consumers -.->|blob storage| apiCele două moduri de rulare (aceeași bază de cod):
| Mod | Ce rulează | Pentru cine |
|---|---|---|
| Headless (microserviciu, ca GLog) | gstorage + Registry (+ MinIO, Postgres, RabbitMQ) | Un microserviciu/sistem care are nevoie de un „raft de stocare” prin API/SDK. Fără UI. Autentificare prin cheie API locală sau token Keycloak. |
| Platformă completă | gateway + gstorage + Registry + toate serviciile | Portal de date deschise + board de administrare, integrat în gStack (Keycloak, GLog, GNotify). |
2. Component diagram (microserviciul gstorage)
Section titled “2. Component diagram (microserviciul gstorage)”flowchart LR subgraph gstorage["gstorage :8100"] subgraph web["web.rest"] rest["*Resource<br/>(admin API, DTO)"] v1["web.rest.v1.*<br/>(/api/v1 mașină, hand-written)"] adminx["AdminStorageResource<br/>(upload/download/share UI)"] end subgraph sec["security"] apikey["apikey.ApiKeyFilter<br/>(cheie API locală)"] oauth["oauth2 resource-server<br/>(token serviciu Keycloak)"] acl["acl.AccessControlService<br/>(niveluri + Permisiune)"] end subgraph svc["service"] core["*Service / *QueryService<br/>(MapStruct, JPA Criteria)"] storage["storage.StorageBackend<br/>(MinioBackend | S3Backend)"] transfer["transfer.*<br/>(sesiuni multipart, Range)"] search["search.*<br/>(index ES + Tika)"] alarm["alarm.AlarmEngine"] arhiva["arhiva.ArchiveService"] connectors["connect.GlogClient / GnotifyClient<br/>(SDK, client-credentials)"] end subgraph ftp["ftp (Apache MINA)"] ftpsrv["FtpServer + FileSystemView<br/>(punte → StorageBackend)"] end subgraph mql["messaging"] rmq["config.RabbitMqConfig<br/>@RabbitListener workers"] end repo["repository.* (JPA)<br/>repository.search.* (ES)"] end
rest --> core v1 --> apikey & oauth --> acl --> core adminx --> acl core --> repo core --> storage --> minio[("MinIO/S3")] transfer --> storage core --> mq2[["RabbitMQ"]] --> rmq rmq --> search --> esx[("Elasticsearch")] rmq --> arhiva --> storage rmq --> connectors alarm --> connectors ftpsrv --> storage repo --> pg[("PostgreSQL")]Straturi (convenția JHipster — entitățile nu părăsesc stratul de service):
Angular SPA (gateway) --HTTPS+JWT/OIDC--> gateway (Spring Cloud Gateway) --/services/gstorage/**--> gstorage web.rest.*Resource (DTO) -> service.*Service (+impl) -> service.*QueryService (JPA Criteria) -> service.mapper (MapStruct) -> repository.*Repository -> PostgreSQL -> service.storage.StorageBackend -> MinIO/S3 (binarele, niciodată în DB) -> repository.search.* -> Elasticsearch- Binarele nu intră în DB —
ObiectStocate doar metadate; octeții stau în object-store prinStorageBackend. /api/v1/**e scris de mână (regenerare-safe), separat de API-ul admin generat.- Doar
ObiectStocat,Dataset,Resursasunt oglindite în Elasticsearch.
3. Object-store: driverul StorageBackend
Section titled “3. Object-store: driverul StorageBackend”Un singur seam de abstracție izolează object-store-ul; MinIO acum, S3 mai târziu, fără schimbări de cod în restul aplicației.
flowchart TB callers["service.* / transfer.* / ftp.* / arhiva.*"] --> iface["interface StorageBackend"] iface --> minio["MinioBackend (dev/demo)"] iface --> s3["S3Backend (prod)"] minio --> m[("MinIO")] s3 --> a[("AWS S3")]public interface StorageBackend { PutResult put(String bucket, String key, InputStream data, long size, String contentType, Map<String,String> meta); GetResult get(String bucket, String key, String version); // stream GetResult getRange(String bucket, String key, long start, long end); // HTTP Range / reluare void softDelete(String bucket, String key); // tombstone void purge(String bucket, String key, String version); List<ObjectVersion> versions(String bucket, String key); // upload reluabil (multipart) String initiateMultipart(String bucket, String key, String contentType); PartResult uploadPart(String bucket, String key, String uploadId, int partNumber, InputStream data, long size); PutResult completeMultipart(String bucket, String key, String uploadId, List<Part> parts); void abortMultipart(String bucket, String key, String uploadId); // ops de fiabilitate — delegate driverului void enableVersioning(String bucket); void replicateTo(String bucket, String targetBackend); // MinIO bucket-replication / S3 CRR void copyForBackup(String bucket, String prefix, String backupBucket); void setLifecycle(String bucket, ClasaStocare tier, int days); // tiering STANDARD→ARHIVA}Fiabilitate (cerința „2–3 replici + backup + integritate”):
- Integritate: sha-256 calculat la ingest, stocat în
ObiectStocat.hashSha256, re-verificat la finalizarea unui transfer și la citirile de audit. - Soft-delete + tombstone:
statut=STERS+stersLa; obiectul rămâne recuperabil în fereastra de retenție; purjarea șterge binarul dar păstrează rândul de metadate + evenimentele (audit permanent). - Versionare: driverul păstrează versiuni;
ObiectStocat.versiune= id-ul. - Replicare:
replicaCountpe obiect; un singur MinIO în demo, darreplicateTo(...)este implementat pe driver (MinIO bucket-replication acum, S3 Cross-Region Replication nativ mai târziu). Multi-nod real = decizie de infrastructură, nu de cod. - Backup: job
@Scheduled→ worker RabbitMQ →copyForBackup(...)către un bucket/prefix de backup.
4. Model de acces (ACL)
Section titled “4. Model de acces (ACL)”Trei niveluri, impuse în stratul de service (nu doar UI), plus granturi explicite pentru resurse restricționate.
flowchart TB req["Cerere: (subiect, drept, resursă)"] --> lvl{"nivelAcces al resursei?"} lvl -->|PUBLIC| readok["CITIRE permisă oricui<br/>(scriere doar proprietar/operator)"] lvl -->|PRIVAT| own{"subiect ∈ organizația<br/>proprietară?"} lvl -->|RESTRICTIONAT| grant{"există Permisiune<br/>(subiect → resursă, drept ≥ cerut,<br/>ne-expirată)?"} own -->|da| scope own -->|nu| grant grant -->|da| scope{"token/cheie are scope-ul<br/>storage:read|write|admin<br/>+ tenant potrivit?"} grant -->|nu| deny["403"] scope -->|da| allow["✔ permis + Eveniment audit"] scope -->|nu| deny- Subiectul = un
AppUser(uman) sau unApiClient(mașină). - Scope-uri (token Keycloak sau cheie API):
storage:read,storage:write,storage:admin; toate purtate cu claim/atributtenant. - Granturile (
Permisiune) leagă un subiect de unbucketsaudatasetcu undreptși expirare opțională; rezultatul aprobării unei cereri din portal. - Descărcarea publică (PUBLIC) poate folosi URL-uri pre-semnate; datele PRIVAT/RESTRICTIONAT se streamează prin backend (ACL impus).
5. Ciclul de viață al unui obiect
Section titled “5. Ciclul de viață al unui obiect”stateDiagram-v2 [*] --> INITIAT: initiate transfer (multipart) INITIAT --> IN_CURS: uploadPart IN_CURS --> IN_CURS: uploadPart (reia de unde s-a oprit) IN_CURS --> ACTIV: complete + sha-256 OK → ObiectStocat INITIAT --> ABANDONAT: abort / expirare IN_CURS --> ESUAT: sha-256 mismatch ACTIV --> ACTIV: nouă versiune (versionare) ACTIV --> STERS: soft-delete (tombstone + stersLa) STERS --> ACTIV: restore (în fereastra de retenție) STERS --> [*]: purge (binar șters, rând + audit păstrate) ACTIV --> ARHIVAT: lifecycle / arhivare (comprimat) ARHIVAT --> ACTIV: restaurare din arhivăFiecare tranziție scrie un rând în Eveniment (append-only) și, dacă e configurat,
trimite același eveniment către GLog.
6. Flux — încărcare reluabilă („continuă de unde s-a oprit”)
Section titled “6. Flux — încărcare reluabilă („continuă de unde s-a oprit”)”sequenceDiagram participant C as Client (SDK) participant A as gstorage /api/v1 participant S as StorageBackend (MinIO) participant Q as RabbitMQ participant W as Worker (Tika/ES)
C->>A: POST /api/v1/transfer (bucket, cheie, dimensiune) A->>S: initiateMultipart() S-->>A: uploadId A-->>C: 201 {uploadId, marimeParte} loop pentru fiecare parte (reluabil) C->>A: PUT /api/v1/transfer/{uploadId}/part/{n} A->>S: uploadPart(n) A-->>C: 200 {etag, octetiIncarcati} end Note over C,A: conexiune pierdută → GET /transfer/{uploadId}<br/>întoarce octetiIncarcati → clientul reia C->>A: POST /api/v1/transfer/{uploadId}/complete (sha256) A->>S: completeMultipart() A->>A: verifică sha-256 → creează ObiectStocat (ACTIV) A->>Q: publish „extract+index" (cheie, bucket) A-->>C: 200 {obiectId, hash, versiune} Q->>W: consumă W->>S: get() → Tika extrage text W->>W: index ES (conținut + metadate)7. Căutare — pipeline Tika + Elasticsearch
Section titled “7. Căutare — pipeline Tika + Elasticsearch”flowchart LR up["upload / resursă nouă"] --> ev["Eveniment UPLOAD"] --> pub["publish RabbitMQ"] pub --> w["Worker extract"] w --> tika["Apache Tika<br/>PDF/DOCX/CSV/XLSX → text"] tika --> idx["index Elasticsearch<br/>(textIndexat + metadate + fațete)"] idx --> es[("ES")] subgraph query["Căutare"] portal["Portal public / board"] --> qsvc["SearchService"] api2["/api/v1/search"] --> qsvc qsvc --> es end- Căutare după nume (metadate în Postgres, exact/prefix) și după conținut (words-in-files, Elasticsearch full-text).
- Fațete/filtre: dată, emitent (organizație), grup/domeniu, public/privat,
format, cuvinte-cheie (
etichete). - Text extras stocat în
ObiectStocat.textIndexat(mirror ES); pentru resurse PRIVAT/RESTRICTIONAT conținutul nu apare în căutarea publică (ACL la query).
8. Integrări & alternative built-in
Section titled “8. Integrări & alternative built-in”| Capabilitate | Conectat la gStack | Alternativă built-in (standalone) |
|---|---|---|
| Logging | connect.GlogClient — SDK GLog, client-credentials (glog-ingest), trimite fiecare Eveniment și la GLog | Eveniment append-only în Postgres (jurnal intern, forensic) |
| Alarme/Notificări | connect.GnotifyClient — SDK GNotify, canal EMAIL/PUSH | AlarmEngine evaluează regulile Alarma → WEBHOOK / LOG |
| Arhivă | — | ArchiveService comprimă N obiecte → obiect ZIP (worker) |
| Scheduler | GScheduler (când există) | joburi @Scheduled (backup, purjare tombstone, expirare sesiuni) |
Ambele povești funcționează: „conectat la gStack” (Keycloak + GLog + GNotify live) și „standalone” (DB local + alarme/loguri interne, fără Keycloak).
9. Autentificare
Section titled “9. Autentificare”flowchart TB subgraph human["UI (gateway) — dual, ca glog/gnotify"] kc["Keycloak OIDC (realm gstorage)"] jwt["username/parolă → JWT HS512 local"] end subgraph machine["/api/v1 (gstorage) — dual"] cc["Keycloak client-credentials<br/>scope storage:* + tenant"] ak["Cheie API locală (ApiClient)<br/>secret hash-uit, fără Keycloak"] end human --> gwsec["gateway SecurityFilterChain<br/>(JwtIssuerAuthenticationManagerResolver)"] machine --> apisec["gstorage: ApiKeyFilter + OAuth2 resource-server<br/>→ AccessControlService"]- UI: login dual — buton Keycloak și formular user/parolă (JWT local
HS512, ca modelul dualauth din glog). Realm Keycloak
gstorage. - Mașină: rezolvat pe token —
issKeycloak → validare JWKS; lipsă token dar antetX-Api-Key→security/apikeyverificăApiClient.apiKeyHash. Ambele produc aceleași authorities (SCOPE_storage:*) +tenant. - Auto-provisioning: la prima conectare a unei organizații ca PaaS (cu domeniu)
se creează automat
Organizatie+Bucket+ApiClient(cerința funcțională).
10. Scalare & fiabilitate
Section titled “10. Scalare & fiabilitate”flowchart TB gw["gateway (LB client-side, Spring Cloud LoadBalancer)"] --> r1["gstorage #1"] gw --> r2["gstorage #2"] r1 & r2 --> reg{{"Eureka (health + discovery = supervizor)"}} r1 & r2 --> mq[["RabbitMQ (cozi de lucru)"]] mq --> retry["retry [10,30,60,120,300]s + DLQ"]- Replici: N instanțe
gstorageînregistrate în Eureka; gateway-ul face load-balancing. Demo: 2 replici (dial la 1 dacă RAM-ul e strâns). - Supervizor: Eureka health +
restart: always+ healthcheck Docker dez-înregistrează instanțele moarte. - Workeri + retry: RabbitMQ, backoff
[10,30,60,120,300]s, DLQ (ca gnotify). - Keep-alive: healthcheck-uri Actuator +
restart: alwayspe toate containerele. - State partajat: metadatele în Postgres, binarele în object-store → orice replică servește orice cerere (stateless); sesiunile de transfer sunt în DB.
11. Topologie de deployment (demo)
Section titled “11. Topologie de deployment (demo)”flowchart TB edge["nginx edge<br/>gstorage.gstack.esempla.systems"] --> gwc["gateway :8080"] gwc --> s1["gstorage :8100 ×2"] s1 --> pg[("postgres:17")] s1 --> es[("elasticsearch")] s1 --> mq[["rabbitmq:3.13"]] s1 --> minio[("minio")] s1 & gwc --> reg{{"jhipster-registry :8761"}} gwc -.-> kc["Keycloak (realm gstorage)"] s1 -.-> glog["GLog"] & gnotify["GNotify"]Rețele Docker: internal (servicii) + external gstack-web (edge). Imagini
împinse în registry.esempla.systems/govtech/gstack/paas_gstorage:{gateway,gstorage}-<ver>.
Detalii în implementation_plan.md §Fază 6.
12. Decizii deschise (de revizitat înainte de hardening)
Section titled “12. Decizii deschise (de revizitat înainte de hardening)”- Multi-nod MinIO real (erasure coding / site-replication) vs S3 nativ — decizie
de infrastructură; codul e deja pregătit prin
StorageBackend. - Unicitatea
ObiectStocat (bucket, cheie, versiune)— changelog dedicat. - Retenția tombstone (soft-delete) + fereastra de restaurare (implicit 30 zile).
- FTP vs FTPS vs SFTP — MINA suportă toate; demo = SFTP + FTPS.
- Conversia efectivă la
jsonb(acumTextBlob) — pas de hardening. - Cuantumul de retenție al jurnalului
Eveniment(append-only, nelimitat implicit).