DMS Module¶
Türkçe
DMS (Document Management System) modülü, dosya yükleme/indirme, güvenli paylaşım linkleri, metin çıkarma (PDF/DOCX/XLSX), kategori ağacı, kota yönetimi ve denetim kaydı sağlar. Depolama pluggable mimaride tasarlanmıştır (LOCAL, S3, Azure, FTP).
Overview¶
The DMS module provides enterprise document management with pluggable storage, security-first
file handling, and full-text search capabilities. It runs as a standalone Spring Boot service on
port 8030 with context path /icdms/services.
┌──────────────────────────────────────────────────────────┐
│ DMS Module — icdms-services (port 8030) │
├──────────────────────────────────────────────────────────┤
│ Sub-Projects: │
│ ├ ICoSys-DMS-Types-1.0 → Enums (no Spring) │
│ ├ ICoSys-DMS-Entities-1.0 → JPA Entities │
│ ├ ICoSys-DMS-Entities-Dto-1.0 → DTOs │
│ └ ICoSys-DMS-Service-1.0 → Spring Boot Service │
│ │
│ Database: icdms (MySQL 8) │
│ ID Table: s_icdms_id │
└──────────────────────────────────────────────────────────┘
Entity Relationship Diagram¶
DmsFile (1) ──────→ (0..1) DmsFileContent (text extraction)
│
├──→ (*) DmsShareLink (public sharing)
│
└──→ (*) DmsAuditLog (action tracking)
DmsCategory (self-referencing tree)
└──→ (*) DmsFile (categorization)
DmsAccountConfig (1 per tenant) (storage settings)
DmsRetentionPolicy (N per tenant) (lifecycle rules)
All entities extend AbstractICEntity6 (crProcess tenant isolation)
Entities¶
DmsFile¶
The core entity representing an uploaded document.
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
Long | PK, auto-gen | Primary key |
| File Info | |||
originalFileName |
String(500) | Original file name as uploaded | |
storedFileName |
String(100) | UUID-based file name on disk | |
fileExtension |
String(20) | File extension (pdf, docx, jpg) | |
mimeType |
String(100) | MIME type (application/pdf) | |
fileSize |
Long | File size in bytes | |
checksum |
String(64) | SHA-256 hash of file content | |
| Storage | |||
storagePath |
String(1000) | Relative path: /{crProcessId}/{entityType}/{entityId}/{uuid}.ext |
|
storageProvider |
StorageProviderType | Storage backend (LOCAL, S3, etc.) | |
| Entity Link | |||
entityType |
String(100) | Related entity type (e.g., "Contract") | |
refEntityId |
Long | Related entity ID | |
integrationKey |
String(200) | Integration key (module:entity:id) |
|
| Metadata | |||
description |
String(1000) | File description | |
tags |
String(500) | Comma-separated tags | |
category |
DmsCategory | FK, optional | Document category |
| Status | |||
status |
FileStatus | default ACTIVE | Lifecycle status |
uploadedBy |
Long | Uploader user ID | |
uploadedAt |
Instant | Upload timestamp | |
deletedAt |
Instant | Soft delete timestamp | |
deletedBy |
Long | Deleter user ID |
DmsFileContent¶
Stores extracted text content for full-text search.
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
Long | PK | Primary key |
fileId |
Long | UNIQUE, NOT NULL | Referenced DmsFile ID |
extractedText |
String(LONGTEXT) | Extracted text content | |
charCount |
Integer | Character count | |
extractionStatus |
TextExtractionStatus | PENDING, PROCESSING, COMPLETED, FAILED, NOT_SUPPORTED | |
extractionError |
String(500) | Error message on failure | |
extractedAt |
Instant | Extraction completion timestamp |
DmsCategory¶
Hierarchical document categories using materialized path.
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
Long | PK | Primary key |
parentId |
Long | nullable | Parent category ID (null = root) |
level |
Integer | Depth level (0 = root) | |
path |
String(500) | Materialized path (e.g., "/1/5/12") | |
code |
String(50) | UNIQUE per tenant | Category code (FATURA, EK, TUTANAK) |
name |
String(200) | NOT NULL | Display name |
description |
String(500) | Description | |
color |
String(20) | Badge color (blue, green, red) | |
icon |
String(50) | Icon class | |
sortOrder |
Integer | Sort order | |
active |
Boolean | default TRUE | Active flag |
Türkçe
Kategori ağacı materialized path tekniğiyle saklanır. /1/5/12 formatı ağaçta
hızlı alt-ağaç sorguları yapılmasını sağlar. level alanı derinlik bilgisi tutar.
DmsShareLink¶
Public download links with optional password protection and limits.
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
Long | PK | Primary key |
fileId |
Long | NOT NULL | Referenced file ID |
token |
String(100) | UNIQUE | UUID share token |
expiresAt |
Instant | nullable | Expiry timestamp (null = never) |
maxDownloads |
Integer | nullable | Max download count (null = unlimited) |
downloadCount |
Integer | default 0 | Current download count |
passwordHash |
String(200) | nullable | BCrypt password hash |
createdByUserId |
Long | Creator user ID | |
active |
Boolean | default TRUE | Active flag |
DmsAccountConfig¶
Per-tenant storage and security configuration.
| Field | Type | Description |
|---|---|---|
id |
Long | Primary key |
| Provider | ||
providerType |
StorageProviderType | Storage backend (LOCAL, S3, AZURE_BLOB, FTP) |
localBasePath |
String(500) | Base path for local storage |
| Limits | ||
maxFileSizeMb |
Integer (default 50) | Maximum file size in MB |
allowedExtensions |
String(1000) | Comma-separated allowed extensions |
allowedMimeGroup |
AllowedMimeGroup | DOCUMENT, IMAGE, ARCHIVE, or ALL |
quotaMb |
Long | Storage quota in MB (null = unlimited) |
| Security | ||
signedUrlExpirySeconds |
Integer (default 300) | Signed URL expiry |
magicBytesValidation |
Boolean (default true) | Enable magic bytes check |
encryptionEnabled |
Boolean (default false) | At-rest encryption |
antivirusEnabled |
Boolean (default false) | Antivirus scanning |
| S3 Settings | ||
s3AccessKey |
String (encrypted) | S3 access key |
s3SecretKey |
String (encrypted) | S3 secret key |
s3Bucket, s3Region, s3Endpoint |
String | S3 configuration |
| Azure Settings | ||
azureConnectionString |
String (encrypted) | Azure connection string |
azureContainer |
String | Azure container name |
| FTP Settings | ||
ftpHost, ftpPort, ftpUsername |
String/Integer | FTP connection |
ftpPassword |
String (encrypted) | FTP password |
ftpBasePath |
String | FTP base path |
| Features | ||
textExtractionEnabled |
Boolean (default false) | Enable text extraction |
active |
Boolean (default true) | Active flag |
DmsRetentionPolicy¶
Automatic lifecycle rules for documents.
| Field | Type | Description |
|---|---|---|
id |
Long | Primary key |
entityType |
String(100) | Target entity type (null = all) |
category |
String(100) | Target category (null = all) |
retentionDays |
Integer | Retention period in days |
action |
FileStatus | Action after retention (ARCHIVED or DELETED) |
notifyDaysBefore |
Integer | Days before expiry to notify |
active |
Boolean (default true) | Active flag |
DmsAuditLog¶
Immutable audit trail for all file operations.
| Field | Type | Description |
|---|---|---|
id |
Long | Primary key |
fileId |
Long | Referenced file ID |
action |
DmsAuditAction | Action type |
userId |
Long | User who performed action |
ipAddress |
String(45) | Client IP address |
userAgent |
String(500) | Client user agent |
details |
String(2000) | JSON details |
actionAt |
Instant | Timestamp |
Enums¶
FileStatus¶
| Value | Description |
|---|---|
ACTIVE |
File is active and available |
ARCHIVED |
File has been archived |
DELETED |
File has been soft-deleted |
StorageProviderType¶
| Value | Description |
|---|---|
LOCAL |
Local disk storage (default) |
S3 |
AWS S3 or MinIO compatible |
AZURE_BLOB |
Azure Blob Storage |
FTP |
FTP/SFTP storage |
DmsAuditAction¶
| Value | Description |
|---|---|
UPLOAD |
File uploaded |
DOWNLOAD |
File downloaded |
VIEW |
File metadata viewed |
DELETE |
File soft-deleted |
RESTORE |
File restored |
ARCHIVE |
File archived |
TextExtractionStatus¶
| Value | Description |
|---|---|
PENDING |
Waiting to be processed |
PROCESSING |
Currently being processed |
COMPLETED |
Successfully extracted |
FAILED |
Extraction failed |
NOT_SUPPORTED |
File type not supported |
AllowedMimeGroup¶
| Value | Allowed Extensions |
|---|---|
DOCUMENT |
pdf, doc, docx, xls, xlsx, ppt, pptx, txt, csv |
IMAGE |
jpg, jpeg, png, gif, bmp, webp, svg |
ARCHIVE |
zip, rar, 7z, tar, gz |
ALL |
All whitelisted file types |
Storage Architecture¶
Türkçe
Depolama katmanı pluggable bir StorageProvider interface'i üzerine kuruludur.
Şu an sadece LocalStorageProvider aktif olup, S3/Azure/FTP gelecekte eklenecektir.
Her kiracı (tenant) DmsAccountConfig üzerinden kendi depolama ayarlarını yapılandırabilir.
StorageProvider Interface¶
public interface StorageProvider {
StorageResult store(InputStream inputStream, String storagePath);
InputStream retrieve(String storagePath);
boolean delete(String storagePath);
boolean exists(String storagePath);
}
Storage Path Format¶
/{crProcessId}/{entityType}/{entityId}/{uuid}.{extension}
Example:
/42/Contract/1523/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf
StorageResult¶
public record StorageResult(
String storagePath, // Actual stored path
long fileSize, // File size in bytes
String checksum // SHA-256 hash
) {}
Upload Flow¶
Client (multipart)
│
▼
DmsFileController.upload()
│
├── 1. Validate crProcessId
├── 2. Check quota (DmsQuotaService)
├── 3. Validate file:
│ ├── Extension whitelist
│ ├── File size limit
│ └── Magic bytes validation (OWASP)
├── 4. Store file (StorageProvider)
│ └── Returns: storagePath + SHA-256 checksum
├── 5. Create DmsFile entity
├── 6. Log audit (UPLOAD action)
├── 7. Trigger async text extraction (if enabled)
│
└── Return DmsFileUploadResponse
├── fileId
├── originalFileName
├── storedFileName
├── fileSize
├── mimeType
├── checksum (SHA-256)
└── uploadedAt
Security¶
Signed URLs (HMAC-SHA256)¶
Download URLs are time-limited and cryptographically signed.
URL format:
/api/dms-file/download/{fileId}?token={HMAC}&expires={timestamp}
Token computation:
input = "{fileId}:{expiresTimestamp}"
secret = dms.security.signed-url-secret
token = Base64URL(HMAC-SHA256(secret, input))
| Property | Default | Description |
|---|---|---|
dms.default.signed-url-expiry-seconds |
300 | URL validity (5 minutes) |
dms.security.signed-url-secret |
env variable | HMAC secret key |
Validation steps:
- Check
expirestimestamp — reject if past - Recompute HMAC from
fileId+expires - Compare with provided
token— reject if mismatch
Magic Bytes Validation (OWASP)¶
Prevents extension spoofing by validating file content signatures.
| Extension | Magic Bytes | Hex |
|---|---|---|
%PDF |
25 50 44 46 |
|
| JPG/JPEG | FF D8 FF |
|
| PNG | 89 50 4E 47 0D 0A 1A 0A |
|
| GIF | GIF8 |
47 49 46 38 |
| ZIP/DOCX/XLSX/PPTX | PK.. |
50 4B 03 04 |
| RAR | Rar! |
52 61 72 21 |
| 7Z | 37 7A BC AF 27 1C |
|
| BMP | BM |
42 4D |
| WEBP | RIFF + WEBP marker at offset 8 |
52 49 46 46 |
Türkçe
Magic bytes doğrulaması dosya uzantısının gerçek içerikle eşleşip eşleşmediğini kontrol eder.
Bu, bir .pdf uzantılı dosyanın gerçekten PDF olup olmadığını doğrular ve
zararlı dosya yükleme saldırılarını önler (OWASP File Upload Cheat Sheet).
Share Link Security (BCrypt)¶
Share links can be password-protected with BCrypt hashing.
Create:
1. Generate UUID token
2. BCrypt hash password (if provided)
3. Store hash in passwordHash field
4. Return share URL: /api/dms-share/d/{token}
Download:
1. Find share link by token
2. Validate: active, not expired, within download limits
3. Validate password (if set) via BCrypt compare
4. Increment downloadCount
5. Log audit event
6. Stream file to client
Download Security Headers¶
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
X-Frame-Options: DENY (or SAMEORIGIN for inline)
HTTP Range support: The download endpoint supports Range headers for video/audio seeking,
returning 206 Partial Content with Content-Range and Accept-Ranges: bytes headers.
Text Extraction¶
Türkçe
Metin çıkarma özelliği DmsAccountConfig.textExtractionEnabled ile tenant bazında açılır.
Dosya yüklendikten sonra @Async olarak arka planda çalışır.
Çıkarılan metin DmsFileContent tablosunda saklanır ve tam metin arama için kullanılır.
Supported Formats¶
| Format | Library | Description |
|---|---|---|
| Apache PDFBox | Extract all text from all pages | |
| DOCX | Apache POI (XWPFWordExtractor) | Extract paragraphs and tables |
| XLSX | Apache POI (XSSFExcelExtractor) | Extract cell values |
| TXT | Java I/O | Read as plain text |
| CSV | Java I/O | Read as plain text |
Extraction Flow¶
1. Upload completes
↓
2. DmsFileService.triggerTextExtraction()
↓ (if textExtractionEnabled)
3. TextExtractionService.createPendingRecord() → DmsFileContent (PENDING)
↓
4. @Async TextExtractionService.extractText()
├── Read file from storage
├── Parse by extension (PDFBox / POI / plain text)
├── Store extractedText + charCount
└── Update status: COMPLETED or FAILED
Full-Text Search¶
// MySQL FULLTEXT index on extracted_text column
// Also searches: originalFileName, tags, description
List<DmsFileListDto> fullTextSearch(Long crProcessId, String searchTerm);
Quota Management¶
public class DmsQuotaService {
long getUsedBytes(Long crProcessId); // Sum of active file sizes
void checkQuota(Long crProcessId, long newFileSize); // Throws if exceeded
DmsQuotaDto getQuotaUsage(Long crProcessId); // Detailed usage
}
// DmsQuotaDto
{
quotaMb: 5120, // 5 GB limit
usedMb: 3200, // 3.2 GB used
availableMb: 1920, // 1.9 GB available
usagePercent: 62.5 // 62.5% used
}
Quota is checked before each upload. If no DmsAccountConfig exists or quotaMb is null,
the upload proceeds without limit.
Audit Trail¶
The DmsAuditService records all file operations with @Transactional(propagation = REQUIRES_NEW)
to ensure audit records survive caller rollbacks.
| Action | Logged When |
|---|---|
UPLOAD |
File uploaded |
DOWNLOAD |
File downloaded (authenticated or share link) |
VIEW |
File metadata accessed |
DELETE |
File soft-deleted |
RESTORE |
File restored from deleted state |
ARCHIVE |
File archived |
Captured data per event:
{
"fileId": 1234,
"action": "UPLOAD",
"userId": 56,
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0 ...",
"details": "{\"fileName\": \"report.pdf\", \"fileSize\": 2621440}",
"actionAt": "2026-02-15T14:30:00Z"
}
REST API¶
Base path: /icdms/services/api
DmsFile — /api/dms-file¶
| Method | Endpoint | Description |
|---|---|---|
| POST | /upload |
Upload file (multipart: file + metadata) |
| GET | /{id} |
Get file view (includes signed download URL) |
| PUT | /{id} |
Update file metadata |
| DELETE | /{id} |
Soft delete file |
| POST | /list |
Paginated list with filter |
| GET | /download/{id}?token=...&expires=... |
Download file (signed URL) |
| GET | /{id}/download-url |
Generate signed download URL |
| GET | /by-entity?entityType=...&entityId=... |
Find files by entity |
| GET | /search?crProcessId=...&searchTerm=... |
Full-text search |
| PUT | /{id}/category?categoryId=... |
Update file category |
Upload parameters:
| Param | Type | Required | Description |
|---|---|---|---|
file |
MultipartFile | Yes | File to upload |
crProcessId |
Long | Yes | Tenant ID |
entityType |
String | No | Related entity type |
entityId |
Long | No | Related entity ID |
integrationKey |
String | No | Integration key |
categoryId |
Long | No | Document category |
description |
String | No | File description |
tags |
String | No | Comma-separated tags |
DmsShareLink — /api/dms-share¶
| Method | Endpoint | Description |
|---|---|---|
| POST | / |
Create share link |
| GET | /{id} |
Get share link |
| DELETE | /{id} |
Deactivate share link |
| POST | /list |
Paginated list |
| GET | /d/{token} |
Public download (no auth required) |
DmsCategory — /api/dms-category¶
| Method | Endpoint | Description |
|---|---|---|
| POST | / |
Create category |
| GET | /{id} |
Get category |
| PUT | /{id} |
Update category |
| DELETE | /{id} |
Delete category |
| POST | /list |
Paginated list |
| GET | /tree |
Get full category tree |
| PUT | /move |
Move category in tree |
DmsAccountConfig — /api/dms-account-config¶
Standard CRUD + list endpoints.
DmsRetentionPolicy — /api/dms-retention-policy¶
Standard CRUD + list endpoints.
DmsAuditLog — /api/dms-audit-log¶
Read-only: GET /{id} and POST /list only.
Public Endpoints (No Authentication)¶
/api/dms-share/d/** → Share link download
/api/dms-file/download/** → Signed URL download (token-protected)
/api/health → Health check
/actuator/** → Actuator endpoints
Error Codes¶
| Constant | Message Key | Context |
|---|---|---|
| File Validation | ||
ERROR_FILE_EMPTY |
error.file.empty |
Empty file upload |
ERROR_FILE_TOO_LARGE |
error.file.too_large |
Exceeds size limit |
ERROR_FILE_EXTENSION_NOT_ALLOWED |
error.file.extension_not_allowed |
Extension whitelist |
ERROR_FILE_INVALID_NAME |
error.file.invalid_name |
Invalid file name |
ERROR_FILE_DANGEROUS_EXTENSION |
error.file.dangerous_extension |
Blacklisted extension |
ERROR_FILE_MAGIC_BYTES_MISMATCH |
error.file.magic_bytes_mismatch |
Content mismatch |
ERROR_FILE_PATH_TRAVERSAL |
error.file.path_traversal |
Path traversal attack |
| Storage | ||
ERROR_STORAGE_FAILED |
error.storage.failed |
General storage error |
ERROR_STORAGE_STORE_FAILED |
error.storage.store_failed |
Store operation failed |
ERROR_STORAGE_CHECKSUM_FAILED |
error.storage.checksum_failed |
Checksum mismatch |
ERROR_STORAGE_FILE_NOT_FOUND |
error.storage.file_not_found |
File not on disk |
| Signed URL | ||
ERROR_SIGNED_URL_EXPIRED |
error.signed_url.expired |
URL expired |
ERROR_SIGNED_URL_INVALID |
error.signed_url.invalid |
Invalid HMAC token |
| Quota | ||
ERROR_QUOTA_EXCEEDED |
error.quota.exceeded |
Quota exceeded |
| Category | ||
ERROR_CATEGORY_CODE_DUPLICATE |
error.category.code_duplicate |
Duplicate code |
ERROR_CATEGORY_CIRCULAR_REFERENCE |
error.category.circular_reference |
Circular parent |
ERROR_CATEGORY_HAS_CHILDREN |
error.category.has_children |
Delete with children |
| Share Link | ||
ERROR_SHARE_LINK_EXPIRED |
error.share_link.expired |
Link expired |
ERROR_SHARE_LINK_MAX_DOWNLOADS |
error.share_link.max_downloads |
Download limit reached |
ERROR_SHARE_LINK_INACTIVE |
error.share_link.inactive |
Link deactivated |
ERROR_SHARE_LINK_PASSWORD_REQUIRED |
error.share_link.password_required |
Password needed |
ERROR_SHARE_LINK_PASSWORD_INVALID |
error.share_link.password_invalid |
Wrong password |
Configuration¶
Default Settings (application.properties)¶
dms.default.provider=LOCAL
dms.default.max-file-size-mb=50
dms.default.allowed-extensions=pdf,doc,docx,xls,xlsx,ppt,pptx,txt,csv,
jpg,jpeg,png,gif,bmp,webp,zip,rar,7z
dms.default.signed-url-expiry-seconds=300
dms.default.magic-bytes-validation=true
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
Development Properties¶
| Property | Value |
|---|---|
server.port |
8030 |
server.servlet.context-path |
/icdms/services |
spring.datasource.url |
jdbc:mysql://localhost:3306/icdms |
dms.default.local-base-path |
C:/server/ICOSYS/data/dms |
dms.security.signed-url-secret |
${DMS_URL_SECRET:dev-...} |
Package Structure¶
com.icom.icosys.dms
├── db/ → Entity classes
├── enums/ → Enum types
├── dto/ → All DTO classes
└── service/
├── codes/ → ServiceErrorMsgCodes
├── audit/ → DmsAuditService
├── extraction/ → TextExtractionService
├── quota/ → DmsQuotaService
├── security/ → SignedUrlService
├── storage/ → StorageProvider, LocalStorageProvider
├── validation/ → MagicBytesRegistry
└── zapi/
├── dmsfile/ → DmsFileController, DmsFileService
├── dmssharelink/ → DmsShareLinkController, DmsShareLinkService
├── dmscategory/ → DmsCategoryController, DmsCategoryService
├── dmsaccountconfig/ → DmsAccountConfigController, Service
├── dmsretentionpolicy/ → Controller, Service
└── dmsauditlog/ → Controller, ReaderService