Skip to content

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.


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:

  1. Check expires timestamp — reject if past
  2. Recompute HMAC from fileId + expires
  3. Compare with provided token — reject if mismatch

Magic Bytes Validation (OWASP)

Prevents extension spoofing by validating file content signatures.

Extension Magic Bytes Hex
PDF %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 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
PDF 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
// 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
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