add gitlab ci
This commit is contained in:
@@ -8,6 +8,21 @@ plugins {
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
// Source unique de vérité : VERSION à la racine du dépôt (ex. 0.1.0).
|
||||
val vaultVersion: String = rootProject.file("../VERSION").readText().trim()
|
||||
val vaultVersionCode: Int = run {
|
||||
val parts = vaultVersion.split('.').map { it.toInt() }
|
||||
check(parts.size == 3) { "VERSION doit être major.minor.patch, reçu: $vaultVersion" }
|
||||
parts[0] * 1_000_000 + parts[1] * 1_000 + parts[2]
|
||||
}
|
||||
|
||||
// Signature release : signing.properties (gitignoré) en local, variables
|
||||
// d'env (SIGNING_*) en CI. Jamais de keystore commité.
|
||||
val signingProps = Properties().apply {
|
||||
val f = file("signing.properties")
|
||||
if (f.exists()) f.inputStream().use { load(it) }
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.vaultdrop.mobile"
|
||||
compileSdk = 35
|
||||
@@ -19,8 +34,8 @@ android {
|
||||
// OpenCV/CameraX sont en 4K (warning de compatibilité sur devices
|
||||
// 16K-pages). On reste en 34 tant qu'elles ne sont pas mises à jour.
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
versionCode = vaultVersionCode
|
||||
versionName = vaultVersion
|
||||
|
||||
// OpenCV natif : on conserve uniquement les ABIs ciblés
|
||||
// (arm64 = devices, x86_64 = émulateur) pour garder l'APK raisonnable.
|
||||
@@ -29,6 +44,18 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
val storePath = signingProps.getProperty("storeFile") ?: System.getenv("SIGNING_STORE_FILE")
|
||||
if (storePath != null) {
|
||||
storeFile = file(storePath)
|
||||
storePassword = signingProps.getProperty("storePassword") ?: System.getenv("SIGNING_STORE_PASSWORD")
|
||||
keyAlias = signingProps.getProperty("keyAlias") ?: System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword = signingProps.getProperty("keyPassword") ?: System.getenv("SIGNING_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
@@ -36,6 +63,7 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Signature release Android — copier vers signing.properties (jamais commité)
|
||||
# Alternative CI : mêmes valeurs via les variables d'environnement SIGNING_*.
|
||||
#
|
||||
# 1. Générer un keystore (ne jamais le committer, garder en lieu sûr) :
|
||||
# keytool -genkey -v -keystore release.keystore -alias vaultdrop \
|
||||
# -keyalg RSA -keysize 2048 -validity 10000
|
||||
# 2. Renseigner les valeurs ci-dessous.
|
||||
#
|
||||
# storeFile est relatif au répertoire du module app/ (mobile-kotlin/app).
|
||||
|
||||
storeFile=release.keystore
|
||||
storePassword=CHANGE_ME
|
||||
keyAlias=vaultdrop
|
||||
keyPassword=CHANGE_ME
|
||||
@@ -6,6 +6,7 @@ import com.vaultdrop.mobile.data.remote.dto.ApiEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.ApiErrorEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.DeviceRegistrationDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileOcrDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FolderDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginRequestDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginResponseDto
|
||||
@@ -105,6 +106,10 @@ class ApiClient @Inject constructor(
|
||||
suspend fun getOcrJob(jobId: String): OcrJobDto =
|
||||
unwrap({ apiService.getOcrJob(jobId) })
|
||||
|
||||
/** Extrait OCR server-side d'un fichier visible (viewer+, own ou shared). */
|
||||
suspend fun getFileOcr(fileId: String): FileOcrDto =
|
||||
unwrap({ apiService.getFileOcr(fileId) })
|
||||
|
||||
private suspend fun <T> unwrap(
|
||||
call: suspend () -> Response<ApiEnvelope<T>>,
|
||||
skipUnauthorizedHandling: Boolean = false,
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vaultdrop.mobile.data.remote
|
||||
import com.vaultdrop.mobile.data.remote.dto.ApiEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.DeviceRegistrationDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileOcrDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FolderDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginRequestDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginResponseDto
|
||||
@@ -96,4 +97,14 @@ interface ApiService {
|
||||
suspend fun getOcrJob(
|
||||
@Path("id") jobId: String,
|
||||
): Response<ApiEnvelope<OcrJobDto>>
|
||||
|
||||
/**
|
||||
* Extrait OCR déjà calculé côté serveur pour un fichier visible (viewer+,
|
||||
* own ou shared). Permet à un second device de récupérer le texte sans
|
||||
* re-téléverser les octets (docs/api-v1.md §5).
|
||||
*/
|
||||
@GET("files/{id}/ocr")
|
||||
suspend fun getFileOcr(
|
||||
@Path("id") fileId: String,
|
||||
): Response<ApiEnvelope<FileOcrDto>>
|
||||
}
|
||||
@@ -152,4 +152,14 @@ data class OcrJobDto(
|
||||
/** Requête `POST /ocr/jobs` — `{ "fileId": "…32-hex" }` (docs/api-v1.md §5). */
|
||||
data class OcrSubmitDto(
|
||||
@Json(name = "fileId") val fileId: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Réponse `GET /files/{id}/ocr` — extrait OCR server-side du fichier,
|
||||
* consultable par le user et les grantees (viewer+). `text` "" tant que
|
||||
* l'OCR n'a pas encore atterri sur la ressource (docs/api-v1.md §5).
|
||||
*/
|
||||
data class FileOcrDto(
|
||||
@Json(name = "text") val text: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String? = null,
|
||||
)
|
||||
@@ -26,6 +26,8 @@ import javax.inject.Singleton
|
||||
*
|
||||
* Séquence pour un fichier SAF `processed` + `local-cloud` (resource_id déjà
|
||||
* connu du serveur via l'outbox `create_resource`) :
|
||||
* 0. [syncFromServer] — si un autre device a déjà OCRisé le fichier, le texte
|
||||
* est rapatrié via `GET /files/:id/ocr` (viewer+) **sans** upload ni job ;
|
||||
* 1. [ensurePhysical] — upload multipart **ciblé** (`resourceId`) des octets
|
||||
* locaux : pipeline séparé, hors outbox, qui écrit
|
||||
* `UPLOAD_DIR/<user>/<resource_id>.<ext>` — sans lequel le job OCR
|
||||
@@ -75,6 +77,20 @@ class OcrRepository @Inject constructor(
|
||||
fileDao.updateOcrText(resourceId, text?.takeIf { it.isNotBlank() }, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
/**
|
||||
* Rapatrie l'extrait OCR déjà calculé côté serveur pour un fichier visible
|
||||
* (viewer+, own ou shared). Renvoie le texte persisté en Room, ou null si le
|
||||
* serveur n'a pas encore d'extrait — un second device récupère ainsi le
|
||||
* résultat d'un OCR fait par un autre device, **sans re-téléverser les
|
||||
* octets ni re-soumettre un job** (docs/api-v1.md §5, GET /files/:id/ocr).
|
||||
*/
|
||||
suspend fun syncFromServer(file: FileEntity): String? {
|
||||
val dto = apiClient.getFileOcr(file.resourceId)
|
||||
if (dto.text.isBlank()) return null
|
||||
saveResult(file.resourceId, dto.text)
|
||||
return dto.text
|
||||
}
|
||||
|
||||
/**
|
||||
* OCR auto d'un fichier (un seul à la fois) : marqueur « en cours », upload
|
||||
* multipart ciblé, soumission du job serveur, poll jusqu'au terminal puis
|
||||
@@ -86,6 +102,12 @@ class OcrRepository @Inject constructor(
|
||||
val now = System.currentTimeMillis()
|
||||
fileDao.markOcrQueued(file.resourceId, now, now)
|
||||
return try {
|
||||
// 0. Un autre device a peut-être déjà OCRisé le fichier : si le
|
||||
// serveur a le texte, on le rapatrie sans upload ni job.
|
||||
if (syncFromServer(file) != null) {
|
||||
fileDao.clearOcrQueued(file.resourceId, System.currentTimeMillis())
|
||||
return OcrAutoOutcome.Done
|
||||
}
|
||||
ensurePhysical(file)
|
||||
val jobId = submit(file.resourceId)
|
||||
val final = poll(jobId)
|
||||
|
||||
@@ -77,6 +77,21 @@ class OcrViewModel @Inject constructor(
|
||||
_uiState.value = OcrUiState(resourceId = file.resourceId, uploading = true)
|
||||
activeJob = viewModelScope.launch {
|
||||
try {
|
||||
// Un autre device a peut-être déjà OCRisé ce document : le
|
||||
// serveur a le texte (GET /files/:id/ocr) → on l'affiche sans
|
||||
// re-téléverser les octets ni re-soumettre un job (docs §5).
|
||||
val synced = ocrRepository.syncFromServer(file)
|
||||
if (synced != null) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
resourceId = file.resourceId,
|
||||
uploading = false,
|
||||
status = OcrJobStatus.DONE,
|
||||
text = synced,
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
ocrRepository.ensurePhysical(file)
|
||||
val jobId = ocrRepository.submit(file.resourceId)
|
||||
val final = ocrRepository.poll(jobId) { job ->
|
||||
|
||||
+5
@@ -17,6 +17,7 @@ import com.vaultdrop.mobile.data.remote.ApiService
|
||||
import com.vaultdrop.mobile.data.remote.dto.ApiEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.DeviceRegistrationDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileOcrDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FolderDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginRequestDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginResponseDto
|
||||
@@ -224,4 +225,8 @@ private class FolderMergeApiService : ApiService {
|
||||
override suspend fun getOcrJob(
|
||||
jobId: String,
|
||||
): Response<ApiEnvelope<OcrJobDto>> = Response.success(ApiEnvelope())
|
||||
|
||||
override suspend fun getFileOcr(
|
||||
fileId: String,
|
||||
): Response<ApiEnvelope<FileOcrDto>> = Response.success(ApiEnvelope(data = FileOcrDto(text = "")))
|
||||
}
|
||||
+5
@@ -18,6 +18,7 @@ import com.vaultdrop.mobile.data.remote.ApiService
|
||||
import com.vaultdrop.mobile.data.remote.dto.ApiEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.DeviceRegistrationDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileOcrDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FolderDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginRequestDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginResponseDto
|
||||
@@ -311,4 +312,8 @@ private class MoveProtectionApiService : ApiService {
|
||||
override suspend fun getOcrJob(
|
||||
jobId: String,
|
||||
): Response<ApiEnvelope<OcrJobDto>> = Response.success(ApiEnvelope())
|
||||
|
||||
override suspend fun getFileOcr(
|
||||
fileId: String,
|
||||
): Response<ApiEnvelope<FileOcrDto>> = Response.success(ApiEnvelope(data = FileOcrDto(text = "")))
|
||||
}
|
||||
+47
@@ -15,6 +15,7 @@ import com.vaultdrop.mobile.data.remote.ApiService
|
||||
import com.vaultdrop.mobile.data.remote.dto.ApiEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.DeviceRegistrationDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileOcrDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FolderDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginRequestDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginResponseDto
|
||||
@@ -220,6 +221,43 @@ class OcrRepositoryTest {
|
||||
assertNull(stored?.ocrQueuedAt)
|
||||
assertNull(stored?.ocrText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncFromServer_persiste_le_texte_deja_calcule_coté_serveur`() = runTest {
|
||||
val file = insertFile("content://ocr/synced.txt")
|
||||
apiService.fileOcr = FileOcrDto(text = "texte d'un autre device", updatedAt = "2026-01-01T00:00:00Z")
|
||||
|
||||
val text = repository.syncFromServer(file)
|
||||
|
||||
assertEquals("texte d'un autre device", text)
|
||||
assertEquals("texte d'un autre device", fileDao.getByResourceId(file.resourceId)?.ocrText)
|
||||
assertEquals(1, apiService.getFileOcrCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncFromServer_retourne_null_quand_le_serveur_na_pas_d_extrait`() = runTest {
|
||||
val file = insertFile("content://ocr/no-ocr.txt")
|
||||
apiService.fileOcr = FileOcrDto(text = "")
|
||||
|
||||
val text = repository.syncFromServer(file)
|
||||
|
||||
assertNull(text)
|
||||
assertNull(fileDao.getByResourceId(file.resourceId)?.ocrText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processAuto_reutilise_le_texte_serveur_sans_upload_ni_job`() = runTest {
|
||||
val file = insertFile("content://ocr/reuse.txt")
|
||||
apiService.fileOcr = FileOcrDto(text = "déjà extrait ailleurs")
|
||||
apiService.jobSequence = emptyList()
|
||||
|
||||
val outcome = repository.processAuto(file)
|
||||
|
||||
assertTrue(outcome is OcrAutoOutcome.Done)
|
||||
assertTrue(apiService.uploadedFiles.isEmpty())
|
||||
assertEquals("déjà extrait ailleurs", fileDao.getByResourceId(file.resourceId)?.ocrText)
|
||||
assertNull(fileDao.getByResourceId(file.resourceId)?.ocrQueuedAt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,6 +268,8 @@ private class OcrApiServiceFake : ApiService {
|
||||
var uploadedFiles: MutableList<Pair<String?, RequestBody?>> = mutableListOf()
|
||||
var uploadedBytes: String? = null
|
||||
var jobSequence: List<OcrJobDto> = emptyList()
|
||||
var fileOcr: FileOcrDto = FileOcrDto(text = "")
|
||||
var getFileOcrCalls = 0
|
||||
private var getCalls = 0
|
||||
|
||||
override suspend fun listFolders(): Response<ApiEnvelope<List<FolderDto>>> =
|
||||
@@ -289,6 +329,13 @@ private class OcrApiServiceFake : ApiService {
|
||||
return Response.success(ApiEnvelope(data = job))
|
||||
}
|
||||
|
||||
override suspend fun getFileOcr(
|
||||
fileId: String,
|
||||
): Response<ApiEnvelope<FileOcrDto>> {
|
||||
getFileOcrCalls++
|
||||
return Response.success(ApiEnvelope(data = fileOcr))
|
||||
}
|
||||
|
||||
private fun readBody(body: RequestBody): String {
|
||||
val buffer = okio.Buffer()
|
||||
body.writeTo(buffer)
|
||||
|
||||
+5
@@ -17,6 +17,7 @@ import com.vaultdrop.mobile.data.remote.ApiService
|
||||
import com.vaultdrop.mobile.data.remote.dto.ApiEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.DeviceRegistrationDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileOcrDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FolderDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginRequestDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginResponseDto
|
||||
@@ -268,4 +269,8 @@ private class FakeApiService : ApiService {
|
||||
override suspend fun getOcrJob(
|
||||
jobId: String,
|
||||
): Response<ApiEnvelope<OcrJobDto>> = Response.success(ApiEnvelope())
|
||||
|
||||
override suspend fun getFileOcr(
|
||||
fileId: String,
|
||||
): Response<ApiEnvelope<FileOcrDto>> = Response.success(ApiEnvelope(data = FileOcrDto(text = "")))
|
||||
}
|
||||
Reference in New Issue
Block a user