Sari la conținut

GStorage — Platform Design

Acest conținut nu este încă disponibil în limba selectată.

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.


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| api

Cele două moduri de rulare (aceeași bază de cod):

ModCe 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 serviciilePortal 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 — ObiectStocat e doar metadate; octeții stau în object-store prin StorageBackend.
  • /api/v1/** e scris de mână (regenerare-safe), separat de API-ul admin generat.
  • Doar ObiectStocat, Dataset, Resursa sunt oglindite în Elasticsearch.

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: replicaCount pe obiect; un singur MinIO în demo, dar replicateTo(...) 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.

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 un ApiClient (mașină).
  • Scope-uri (token Keycloak sau cheie API): storage:read, storage:write, storage:admin; toate purtate cu claim/atribut tenant.
  • Granturile (Permisiune) leagă un subiect de un bucket sau dataset cu un drept ș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).

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).

CapabilitateConectat la gStackAlternativă built-in (standalone)
Loggingconnect.GlogClient — SDK GLog, client-credentials (glog-ingest), trimite fiecare Eveniment și la GLogEveniment append-only în Postgres (jurnal intern, forensic)
Alarme/Notificăriconnect.GnotifyClient — SDK GNotify, canal EMAIL/PUSHAlarmEngine evaluează regulile Alarma → WEBHOOK / LOG
Arhivă—ArchiveService comprimă N obiecte → obiect ZIP (worker)
SchedulerGScheduler (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).


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 — iss Keycloak → validare JWKS; lipsă token dar antet X-Api-Key → security/apikey verifică 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ă).

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: always pe toate containerele.
  • State partajat: metadatele în Postgres, binarele în object-store → orice replică servește orice cerere (stateless); sesiunile de transfer sunt în DB.

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 (acum TextBlob) — pas de hardening.
  • Cuantumul de retenție al jurnalului Eveniment (append-only, nelimitat implicit).