Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
apply plugin: 'com.android.library'
apply plugin: 'com.google.devtools.ksp'
apply plugin: 'kotlin-android'
android {
compileSdkVersion sdkCompileVersion
defaultConfig {
minSdkVersion sdkMinVersion
targetSdkVersion sdkTargetVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// The schemas directory contains a schema file for each version of the Room database.
// This is required to enable Room auto migrations.
// See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
sourceSets {
androidTest.java.srcDirs += "src/test-common/java"
test.java.srcDirs += "src/test-common/java"
// Room Database Tests
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
packagingOptions {
resources.excludes.add("META-INF/*")
}
namespace "eu.qsfera.android.data"
}
dependencies {
implementation project(":qsferaDomain")
// QSfera Android Library
api project(":qsferaComLibrary")
// Kotlin
implementation libs.kotlin.stdlib
implementation libs.kotlinx.coroutines.core
// Androidx
implementation libs.androidx.core.ktx
implementation libs.androidx.lifecycle.livedata.ktx
// Room
implementation libs.androidx.room.ktx
ksp libs.androidx.room.compiler
implementation libs.moshi.kotlin
ksp libs.moshi.kotlin.codegen
// Timber
implementation libs.timber
// Dependencies for unit tests
testImplementation project(":qsferaTestUtil")
testImplementation libs.androidx.arch.core.testing
testImplementation libs.junit4
testImplementation libs.kotlinx.coroutines.test
testImplementation libs.mockk
// Dependencies for instrumented tests
androidTestImplementation project(":qsferaTestUtil")
androidTestImplementation libs.androidx.arch.core.testing
androidTestImplementation libs.androidx.room.testing
androidTestImplementation libs.androidx.test.espresso.core
androidTestImplementation libs.androidx.test.ext.junit
androidTestImplementation libs.androidx.test.runner
androidTestImplementation libs.kotlinx.coroutines.test
androidTestImplementation(libs.mockk.android) { exclude module: "objenesis" }
// Detekt
detektPlugins libs.detekt.formatting
detektPlugins libs.detekt.libraries
}
tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach {
jvmTarget = "17"
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,318 @@
{
"formatVersion": 1,
"database": {
"version": 27,
"identityHash": "3d0931837b51601d490be982b5c88fbc",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_source` INTEGER NOT NULL, `item_source` INTEGER NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileSource",
"columnName": "file_source",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemSource",
"columnName": "item_source",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "shate_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_password_enforced_read_only` INTEGER, `sharing_public_password_enforced_read_write` INTEGER, `sharing_public_password_enforced_public_only` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_public_multiple` INTEGER, `supports_upload_only` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicSendMail",
"columnName": "sharing_public_send_mail",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingUserSendMail",
"columnName": "sharing_user_send_mail",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3d0931837b51601d490be982b5c88fbc')"
]
}
}
@@ -0,0 +1,343 @@
{
"formatVersion": 1,
"database": {
"version": 28,
"identityHash": "f901705a4788c535432f7e84cd7ff93e",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_source` INTEGER NOT NULL, `item_source` INTEGER NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileSource",
"columnName": "file_source",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemSource",
"columnName": "item_source",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "shate_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `search_min_length` INTEGER, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_user_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingSearchMinLength",
"columnName": "search_min_length",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSendMail",
"columnName": "sharing_public_send_mail",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingUserSendMail",
"columnName": "sharing_user_send_mail",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f901705a4788c535432f7e84cd7ff93e')"
]
}
}
@@ -0,0 +1,337 @@
{
"formatVersion": 1,
"database": {
"version": 29,
"identityHash": "f5bffaa0e01cfebcee457075ef18df81",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_source` TEXT NOT NULL, `item_source` TEXT NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileSource",
"columnName": "file_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itemSource",
"columnName": "item_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "shate_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_user_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSendMail",
"columnName": "sharing_public_send_mail",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingUserSendMail",
"columnName": "sharing_user_send_mail",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5bffaa0e01cfebcee457075ef18df81')"
]
}
}
@@ -0,0 +1,329 @@
{
"formatVersion": 1,
"database": {
"version": 30,
"identityHash": "3810901cf23b4449e2a761e48c57d015",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_source` TEXT NOT NULL, `item_source` TEXT NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileSource",
"columnName": "file_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itemSource",
"columnName": "item_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "shate_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3810901cf23b4449e2a761e48c57d015')"
]
}
}
@@ -0,0 +1,361 @@
{
"formatVersion": 1,
"database": {
"version": 31,
"identityHash": "b8ca0150fc10f2034d497250e35bf062",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_source` TEXT NOT NULL, `item_source` TEXT NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileSource",
"columnName": "file_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itemSource",
"columnName": "item_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "shate_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b8ca0150fc10f2034d497250e35bf062')"
]
}
}
@@ -0,0 +1,361 @@
{
"formatVersion": 1,
"database": {
"version": 32,
"identityHash": "b8ca0150fc10f2034d497250e35bf062",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_source` TEXT NOT NULL, `item_source` TEXT NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileSource",
"columnName": "file_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itemSource",
"columnName": "item_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "shate_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b8ca0150fc10f2034d497250e35bf062')"
]
}
}
@@ -0,0 +1,343 @@
{
"formatVersion": 1,
"database": {
"version": 33,
"identityHash": "d64c9b3f56ef37b1ff582533f6e4248d",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "share_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd64c9b3f56ef37b1ff582533f6e4248d')"
]
}
}
@@ -0,0 +1,405 @@
{
"formatVersion": 1,
"database": {
"version": 34,
"identityHash": "23b7b67de0e6641230799a550a78bde9",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "share_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder_backup",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "behavior",
"columnName": "behavior",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcePath",
"columnName": "sourcePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploadPath",
"columnName": "uploadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wifiOnly",
"columnName": "wifiOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSyncTimestamp",
"columnName": "lastSyncTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '23b7b67de0e6641230799a550a78bde9')"
]
}
}
@@ -0,0 +1,411 @@
{
"formatVersion": 1,
"database": {
"version": 35,
"identityHash": "7a20a79301de3928a1d96e9d47fda953",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "share_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder_backup",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `chargingOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "behavior",
"columnName": "behavior",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcePath",
"columnName": "sourcePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploadPath",
"columnName": "uploadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wifiOnly",
"columnName": "wifiOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargingOnly",
"columnName": "chargingOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSyncTimestamp",
"columnName": "lastSyncTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7a20a79301de3928a1d96e9d47fda953')"
]
}
}
@@ -0,0 +1,418 @@
{
"formatVersion": 1,
"database": {
"version": 36,
"identityHash": "5dac7fa6dea3bc459730986ec65dbf44",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "share_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `sharing_user_profile_picture` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingUserProfilePicture",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "sharing_user_profile_picture",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder_backup",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `chargingOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "behavior",
"columnName": "behavior",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcePath",
"columnName": "sourcePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploadPath",
"columnName": "uploadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wifiOnly",
"columnName": "wifiOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargingOnly",
"columnName": "chargingOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSyncTimestamp",
"columnName": "lastSyncTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5dac7fa6dea3bc459730986ec65dbf44')"
]
}
}
@@ -0,0 +1,461 @@
{
"formatVersion": 1,
"database": {
"version": 37,
"identityHash": "71d1fadd8d9bbd892399ade177041868",
"entities": [
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "share_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `sharing_user_profile_picture` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1, `files_private_links` INTEGER NOT NULL DEFAULT -1, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `enabled` INTEGER, `version` TEXT, `appsUrl` TEXT, `openUrl` TEXT, `openWebUrl` TEXT, `newUrl` TEXT)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingUserProfilePicture",
"columnName": "sharing_user_profile_picture",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesPrivateLink",
"columnName": "files_private_links",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appProvider.enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "approvider.version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.appsUrl",
"columnName": "appsUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.openUrl",
"columnName": "openUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.openWebUrl",
"columnName": "openWebUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.newUrl",
"columnName": "newUrl",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder_backup",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `chargingOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "behavior",
"columnName": "behavior",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcePath",
"columnName": "sourcePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploadPath",
"columnName": "uploadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wifiOnly",
"columnName": "wifiOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargingOnly",
"columnName": "chargingOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSyncTimestamp",
"columnName": "lastSyncTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '71d1fadd8d9bbd892399ade177041868')"
]
}
}
@@ -0,0 +1,711 @@
{
"formatVersion": 1,
"database": {
"version": 38,
"identityHash": "ebf911554c28d3e8df34bc85546bdccb",
"entities": [
{
"tableName": "folder_backup",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `chargingOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "behavior",
"columnName": "behavior",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcePath",
"columnName": "sourcePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploadPath",
"columnName": "uploadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wifiOnly",
"columnName": "wifiOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargingOnly",
"columnName": "chargingOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSyncTimestamp",
"columnName": "lastSyncTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `sharing_user_profile_picture` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1, `files_private_links` INTEGER NOT NULL DEFAULT -1, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `enabled` INTEGER, `version` TEXT, `appsUrl` TEXT, `openUrl` TEXT, `openWebUrl` TEXT, `newUrl` TEXT)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingUserProfilePicture",
"columnName": "sharing_user_profile_picture",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesPrivateLinks",
"columnName": "files_private_links",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appProvider.enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appProvider.version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.appsUrl",
"columnName": "appsUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.openUrl",
"columnName": "openUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.openWebUrl",
"columnName": "openWebUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.newUrl",
"columnName": "newUrl",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "files",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`parentId` INTEGER, `owner` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `remoteId` TEXT, `length` INTEGER NOT NULL, `creationTimestamp` INTEGER, `modificationTimestamp` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `etag` TEXT, `permissions` TEXT, `privateLink` TEXT, `storagePath` TEXT, `name` TEXT, `treeEtag` TEXT, `keepInSync` INTEGER, `lastSyncDateForData` INTEGER, `fileShareViaLink` INTEGER, `lastSyncDateForProperties` INTEGER, `needsToUpdateThumbnail` INTEGER NOT NULL, `modifiedAtLastSyncForData` INTEGER, `etagInConflict` TEXT, `fileIsDownloading` INTEGER, `sharedWithSharee` INTEGER, `sharedByLink` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "parentId",
"columnName": "parentId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "owner",
"columnName": "owner",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePath",
"columnName": "remotePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "length",
"columnName": "length",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "creationTimestamp",
"columnName": "creationTimestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "modificationTimestamp",
"columnName": "modificationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "etag",
"columnName": "etag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "privateLink",
"columnName": "privateLink",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "storagePath",
"columnName": "storagePath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "treeEtag",
"columnName": "treeEtag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "keepInSync",
"columnName": "keepInSync",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSyncDateForData",
"columnName": "lastSyncDateForData",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "fileShareViaLink",
"columnName": "fileShareViaLink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSyncDateForProperties",
"columnName": "lastSyncDateForProperties",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "needsToUpdateThumbnail",
"columnName": "needsToUpdateThumbnail",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "modifiedAtLastSyncForData",
"columnName": "modifiedAtLastSyncForData",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "etagInConflict",
"columnName": "etagInConflict",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fileIsDownloading",
"columnName": "fileIsDownloading",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "sharedWithSharee",
"columnName": "sharedWithSharee",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "sharedByLink",
"columnName": "sharedByLink",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "share_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "transfers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localPath` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `accountName` TEXT NOT NULL, `fileSize` INTEGER NOT NULL, `status` INTEGER NOT NULL, `localBehaviour` INTEGER NOT NULL, `forceOverwrite` INTEGER NOT NULL, `transferEndTimestamp` INTEGER, `lastResult` INTEGER, `createdBy` INTEGER NOT NULL, `transferId` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "localPath",
"columnName": "localPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePath",
"columnName": "remotePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fileSize",
"columnName": "fileSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localBehaviour",
"columnName": "localBehaviour",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceOverwrite",
"columnName": "forceOverwrite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "transferEndTimestamp",
"columnName": "transferEndTimestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastResult",
"columnName": "lastResult",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "createdBy",
"columnName": "createdBy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "transferId",
"columnName": "transferId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ebf911554c28d3e8df34bc85546bdccb')"
]
}
}
@@ -0,0 +1,761 @@
{
"formatVersion": 1,
"database": {
"version": 39,
"identityHash": "71dab0c694d243e067f4087932cf1c3d",
"entities": [
{
"tableName": "folder_backup",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `chargingOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "behavior",
"columnName": "behavior",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcePath",
"columnName": "sourcePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploadPath",
"columnName": "uploadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wifiOnly",
"columnName": "wifiOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargingOnly",
"columnName": "chargingOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSyncTimestamp",
"columnName": "lastSyncTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "capabilities",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `sharing_user_profile_picture` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1, `files_private_links` INTEGER NOT NULL DEFAULT -1, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `enabled` INTEGER, `version` TEXT, `appsUrl` TEXT, `openUrl` TEXT, `openWebUrl` TEXT, `newUrl` TEXT)",
"fields": [
{
"fieldPath": "accountName",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionMayor",
"columnName": "version_mayor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMinor",
"columnName": "version_minor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionMicro",
"columnName": "version_micro",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionString",
"columnName": "version_string",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionEdition",
"columnName": "version_edition",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "corePollInterval",
"columnName": "core_pollinterval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "davChunkingVersion",
"columnName": "dav_chunking_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filesSharingApiEnabled",
"columnName": "sharing_api_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicEnabled",
"columnName": "sharing_public_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforced",
"columnName": "sharing_public_password_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadOnly",
"columnName": "sharing_public_password_enforced_read_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedReadWrite",
"columnName": "sharing_public_password_enforced_read_write",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly",
"columnName": "sharing_public_password_enforced_public_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateEnabled",
"columnName": "sharing_public_expire_date_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicExpireDateDays",
"columnName": "sharing_public_expire_date_days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filesSharingPublicExpireDateEnforced",
"columnName": "sharing_public_expire_date_enforced",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicUpload",
"columnName": "sharing_public_upload",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicMultiple",
"columnName": "sharing_public_multiple",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingPublicSupportsUploadOnly",
"columnName": "supports_upload_only",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingResharing",
"columnName": "sharing_resharing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationOutgoing",
"columnName": "sharing_federation_outgoing",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingFederationIncoming",
"columnName": "sharing_federation_incoming",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesSharingUserProfilePicture",
"columnName": "sharing_user_profile_picture",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesBigFileChunking",
"columnName": "files_bigfilechunking",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesUndelete",
"columnName": "files_undelete",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesVersioning",
"columnName": "files_versioning",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "filesPrivateLinks",
"columnName": "files_private_links",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appProvider.enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appProvider.version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.appsUrl",
"columnName": "appsUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.openUrl",
"columnName": "openUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.openWebUrl",
"columnName": "openWebUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appProvider.newUrl",
"columnName": "newUrl",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "files",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`parentId` INTEGER, `owner` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `remoteId` TEXT, `length` INTEGER NOT NULL, `creationTimestamp` INTEGER, `modificationTimestamp` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `etag` TEXT, `permissions` TEXT, `privateLink` TEXT, `storagePath` TEXT, `name` TEXT, `treeEtag` TEXT, `keepInSync` INTEGER, `lastSyncDateForData` INTEGER, `fileShareViaLink` INTEGER, `lastSyncDateForProperties` INTEGER, `needsToUpdateThumbnail` INTEGER NOT NULL, `modifiedAtLastSyncForData` INTEGER, `etagInConflict` TEXT, `fileIsDownloading` INTEGER, `sharedWithSharee` INTEGER, `sharedByLink` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "parentId",
"columnName": "parentId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "owner",
"columnName": "owner",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePath",
"columnName": "remotePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "length",
"columnName": "length",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "creationTimestamp",
"columnName": "creationTimestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "modificationTimestamp",
"columnName": "modificationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "etag",
"columnName": "etag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "privateLink",
"columnName": "privateLink",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "storagePath",
"columnName": "storagePath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "treeEtag",
"columnName": "treeEtag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "availableOfflineStatus",
"columnName": "keepInSync",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSyncDateForData",
"columnName": "lastSyncDateForData",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "fileShareViaLink",
"columnName": "fileShareViaLink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSyncDateForProperties",
"columnName": "lastSyncDateForProperties",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "needsToUpdateThumbnail",
"columnName": "needsToUpdateThumbnail",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "modifiedAtLastSyncForData",
"columnName": "modifiedAtLastSyncForData",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "etagInConflict",
"columnName": "etagInConflict",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fileIsDownloading",
"columnName": "fileIsDownloading",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "sharedWithSharee",
"columnName": "sharedWithSharee",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "sharedByLink",
"columnName": "sharedByLink",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "files_sync",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `uploadWorkerUuid` BLOB, `downloadWorkerUuid` BLOB, `isSynchronizing` INTEGER NOT NULL, PRIMARY KEY(`fileId`), FOREIGN KEY(`fileId`) REFERENCES `files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "fileId",
"columnName": "fileId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uploadWorkerUuid",
"columnName": "uploadWorkerUuid",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "downloadWorkerUuid",
"columnName": "downloadWorkerUuid",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "isSynchronizing",
"columnName": "isSynchronizing",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"fileId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "files",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"fileId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ocshares",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "shareType",
"columnName": "share_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shareWith",
"columnName": "share_with",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sharedDate",
"columnName": "shared_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationDate",
"columnName": "expiration_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithDisplayName",
"columnName": "shared_with_display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sharedWithAdditionalInfo",
"columnName": "share_with_additional_info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFolder",
"columnName": "is_directory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "id_remote_shared",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountOwner",
"columnName": "owner_share",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareLink",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_quotas",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))",
"fields": [
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "used",
"columnName": "used",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"accountName"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "transfers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localPath` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `accountName` TEXT NOT NULL, `fileSize` INTEGER NOT NULL, `status` INTEGER NOT NULL, `localBehaviour` INTEGER NOT NULL, `forceOverwrite` INTEGER NOT NULL, `transferEndTimestamp` INTEGER, `lastResult` INTEGER, `createdBy` INTEGER NOT NULL, `transferId` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "localPath",
"columnName": "localPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePath",
"columnName": "remotePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fileSize",
"columnName": "fileSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localBehaviour",
"columnName": "localBehaviour",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceOverwrite",
"columnName": "forceOverwrite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "transferEndTimestamp",
"columnName": "transferEndTimestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastResult",
"columnName": "lastResult",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "createdBy",
"columnName": "createdBy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "transferId",
"columnName": "transferId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '71dab0c694d243e067f4087932cf1c3d')"
]
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,420 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.authentication.datasources.implementation
import android.accounts.Account
import android.accounts.AccountManager
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_ID
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_SECRET
import eu.qsfera.android.data.authentication.KEY_OAUTH2_REFRESH_TOKEN
import eu.qsfera.android.data.authentication.KEY_OAUTH2_SCOPE
import eu.qsfera.android.data.authentication.SELECTED_ACCOUNT
import eu.qsfera.android.data.providers.SharedPreferencesProvider
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
import eu.qsfera.android.domain.exceptions.AccountNotFoundException
import eu.qsfera.android.domain.exceptions.AccountNotNewException
import eu.qsfera.android.domain.exceptions.AccountNotTheSameException
import eu.qsfera.android.domain.server.model.ServerInfo
import eu.qsfera.android.domain.user.model.UserInfo
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.ACCOUNT_VERSION
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_DISPLAY_NAME
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_SUPPORTS_OAUTH2
import eu.qsfera.android.testutil.OC_ACCESS_TOKEN
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_ACCOUNT_ID
import eu.qsfera.android.testutil.OC_ACCOUNT_NAME
import eu.qsfera.android.testutil.OC_AUTH_TOKEN_TYPE
import eu.qsfera.android.testutil.OC_BASIC_PASSWORD
import eu.qsfera.android.testutil.OC_SECURE_BASE_URL
import eu.qsfera.android.testutil.OC_BASIC_USERNAME
import eu.qsfera.android.testutil.OC_OAUTH_SUPPORTED_TRUE
import eu.qsfera.android.testutil.OC_REDIRECTION_PATH
import eu.qsfera.android.testutil.OC_REFRESH_TOKEN
import eu.qsfera.android.testutil.OC_SCOPE
import eu.qsfera.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH
import eu.qsfera.android.testutil.OC_USER_INFO
import eu.qsfera.android.testutil.oauth.OC_CLIENT_REGISTRATION
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class OCLocalAuthenticationDataSourceTest {
private lateinit var ocLocalAuthenticationDataSource: OCLocalAuthenticationDataSource
private val accountManager = mockk<AccountManager>(relaxUnitFun = true)
private val preferencesProvider = mockk<SharedPreferencesProvider>(relaxUnitFun = true)
@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
ocLocalAuthenticationDataSource = OCLocalAuthenticationDataSource(
context,
accountManager,
preferencesProvider,
OC_ACCOUNT.type
)
mockGetAccountsByType(OC_ACCOUNT.type, arrayOf(OC_ACCOUNT))
}
@Test
fun addBasicAccountOk() {
mockRegularAccountCreationFlow()
mockSelectedAccountNameInPreferences()
val newAccountName = ocLocalAuthenticationDataSource.addBasicAccount(
OC_ACCOUNT_ID,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_BASIC_PASSWORD,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO,
null
)
val newAccount = Account(OC_ACCOUNT_NAME, OC_ACCOUNT.type)
assertEquals(newAccount.name, newAccountName)
// One for checking if the account exists and another one for getting the new account
verifyAccountsByTypeAreGot(newAccount.type, 2)
verifyAccountIsExplicitlyAdded(newAccount, OC_BASIC_PASSWORD, 1)
verifyAccountInfoIsUpdated(newAccount, OC_SECURE_SERVER_INFO_BASIC_AUTH, OC_USER_INFO, 1)
}
@Test(expected = AccountNotNewException::class)
fun addBasicAccountAlreadyExistsNoUpdate() {
ocLocalAuthenticationDataSource.addBasicAccount(
OC_ACCOUNT_ID,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_BASIC_PASSWORD,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO.copy(id = OC_ACCOUNT_ID),
null
)
}
@Test
fun addBasicAccountAlreadyExistsUpdateSameUsername() {
mockSelectedAccountNameInPreferences()
val accountName = ocLocalAuthenticationDataSource.addBasicAccount(
OC_ACCOUNT_ID,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_BASIC_PASSWORD,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO.copy(id = OC_ACCOUNT_ID),
OC_ACCOUNT_NAME
)
assertEquals(OC_ACCOUNT_NAME, accountName)
// One for getting account to update
verifyAccountsByTypeAreGot(OC_ACCOUNT.type, 1)
// The account already exists so do not create it
verifyAccountIsExplicitlyAdded(OC_ACCOUNT, OC_BASIC_PASSWORD, 0)
// The account already exists, so update it
verifyAccountInfoIsUpdated(OC_ACCOUNT, OC_SECURE_SERVER_INFO_BASIC_AUTH, OC_USER_INFO, 1)
}
@Test
fun addBasicAccountAlreadyExistsUpdateDifferentUsername() {
mockSelectedAccountNameInPreferences()
try {
ocLocalAuthenticationDataSource.addBasicAccount(
OC_BASIC_USERNAME,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_BASIC_PASSWORD,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO,
"NotTheSameAccount"
)
} catch (exception: Exception) {
assertTrue(exception is AccountNotTheSameException)
} finally {
// The account already exists so do not create a new one
verifyAccountIsExplicitlyAdded(OC_ACCOUNT, OC_BASIC_PASSWORD, 0)
// The account is not the same, so no update needed
verifyAccountInfoIsUpdated(OC_ACCOUNT, OC_SECURE_SERVER_INFO_BASIC_AUTH, OC_USER_INFO, 0)
}
}
@Test
fun addOAuthAccountOk() {
mockRegularAccountCreationFlow()
mockSelectedAccountNameInPreferences()
val newAccountName = ocLocalAuthenticationDataSource.addOAuthAccount(
OC_ACCOUNT_ID,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_AUTH_TOKEN_TYPE,
OC_ACCESS_TOKEN,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO,
OC_REFRESH_TOKEN,
OC_SCOPE,
null,
OC_CLIENT_REGISTRATION
)
val newAccount = Account(OC_ACCOUNT_NAME, OC_ACCOUNT.type)
// One for checking if the account exists and another one for getting the new account
verifyAccountsByTypeAreGot(newAccount.type, 2)
verifyAccountIsExplicitlyAdded(newAccount, "", 1)
verifyAccountInfoIsUpdated(newAccount, OC_SECURE_SERVER_INFO_BASIC_AUTH, OC_USER_INFO, 1)
// OAuth params are updated
verifyOAuthParamsAreUpdated(newAccount, OC_ACCESS_TOKEN, OC_OAUTH_SUPPORTED_TRUE, OC_REFRESH_TOKEN, OC_SCOPE, OC_CLIENT_REGISTRATION, 1)
assertEquals(newAccount.name, newAccountName)
}
@Test(expected = AccountNotNewException::class)
fun addOAuthAccountAlreadyExistsNoUpdate() {
ocLocalAuthenticationDataSource.addOAuthAccount(
OC_ACCOUNT_ID,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_AUTH_TOKEN_TYPE,
OC_ACCESS_TOKEN,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO.copy(id = OC_ACCOUNT_ID),
OC_REFRESH_TOKEN,
OC_SCOPE,
null,
OC_CLIENT_REGISTRATION
)
}
@Test
fun addOAuthAccountAlreadyExistsUpdateSameUsername() {
mockSelectedAccountNameInPreferences()
val accountName = ocLocalAuthenticationDataSource.addOAuthAccount(
OC_ACCOUNT_ID,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_AUTH_TOKEN_TYPE,
OC_ACCESS_TOKEN,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO.copy(id = OC_ACCOUNT_ID),
OC_REFRESH_TOKEN,
OC_SCOPE,
OC_ACCOUNT_NAME,
OC_CLIENT_REGISTRATION
)
assertEquals(OC_ACCOUNT_NAME, accountName)
// One for getting account to update
verifyAccountsByTypeAreGot(OC_ACCOUNT.type, 1)
// The account already exists so do not create it
verifyAccountIsExplicitlyAdded(OC_ACCOUNT, OC_BASIC_PASSWORD, 0)
// The account already exists, so update it
verifyAccountInfoIsUpdated(OC_ACCOUNT, OC_SECURE_SERVER_INFO_BASIC_AUTH, OC_USER_INFO, 1)
verifyOAuthParamsAreUpdated(OC_ACCOUNT, OC_ACCESS_TOKEN, OC_OAUTH_SUPPORTED_TRUE, OC_REFRESH_TOKEN, OC_SCOPE, OC_CLIENT_REGISTRATION, 1)
}
@Test
fun addOAuthAccountAlreadyExistsUpdateDifferentUsername() {
mockSelectedAccountNameInPreferences()
try {
ocLocalAuthenticationDataSource.addOAuthAccount(
OC_BASIC_USERNAME,
OC_REDIRECTION_PATH.lastPermanentLocation,
OC_AUTH_TOKEN_TYPE,
OC_ACCESS_TOKEN,
OC_SECURE_SERVER_INFO_BASIC_AUTH,
OC_USER_INFO,
OC_REFRESH_TOKEN,
OC_SCOPE,
"AccountNotTheSame",
OC_CLIENT_REGISTRATION
)
} catch (exception: Exception) {
assertTrue(exception is AccountNotTheSameException)
} finally {
// The account already exists so do not create it
verifyAccountIsExplicitlyAdded(OC_ACCOUNT, OC_BASIC_PASSWORD, 0)
// The account already exists, so update it
verifyAccountInfoIsUpdated(OC_ACCOUNT, OC_SECURE_SERVER_INFO_BASIC_AUTH, OC_USER_INFO, 0)
verifyOAuthParamsAreUpdated(
OC_ACCOUNT,
OC_ACCESS_TOKEN,
OC_OAUTH_SUPPORTED_TRUE,
OC_REFRESH_TOKEN,
OC_SCOPE,
OC_CLIENT_REGISTRATION,
0
)
}
}
@Test
fun supportsOAuth2Ok() {
every {
accountManager.getUserData(OC_ACCOUNT, KEY_SUPPORTS_OAUTH2)
} returns OC_OAUTH_SUPPORTED_TRUE
val supportsOAuth2 = ocLocalAuthenticationDataSource.supportsOAuth2(OC_ACCOUNT.name)
assertTrue(supportsOAuth2)
verifyAccountsByTypeAreGot(OC_ACCOUNT.type, 1)
verifyUserDataIsGot(OC_ACCOUNT, KEY_SUPPORTS_OAUTH2, 1)
}
@Test(expected = AccountNotFoundException::class)
fun supportsOAuthAccountNotFound() {
mockGetAccountsByType(OC_ACCOUNT.type, arrayOf()) // That account does not exist
ocLocalAuthenticationDataSource.supportsOAuth2(OC_ACCOUNT.name)
}
@Test
fun getBaseUrlOk() {
every {
accountManager.getUserData(OC_ACCOUNT, KEY_OC_BASE_URL)
} returns OC_SECURE_BASE_URL
val baseUrl = ocLocalAuthenticationDataSource.getBaseUrl(OC_ACCOUNT.name)
assertEquals(OC_SECURE_BASE_URL, baseUrl)
verifyAccountsByTypeAreGot(OC_ACCOUNT.type, 1)
verifyUserDataIsGot(OC_ACCOUNT, KEY_OC_BASE_URL, 1)
}
@Test(expected = AccountNotFoundException::class)
fun getBaseUrlAccountNotFound() {
mockGetAccountsByType(OC_ACCOUNT.type, arrayOf()) // That account does not exist
ocLocalAuthenticationDataSource.getBaseUrl(OC_ACCOUNT.name)
}
private fun mockGetAccountsByType(accountType: String, accounts: Array<Account>) {
every {
accountManager.getAccountsByType(accountType)
} returns accounts
}
private fun mockSelectedAccountNameInPreferences(
selectedAccountName: String = OC_ACCOUNT.name
) {
every {
preferencesProvider.getString(SELECTED_ACCOUNT, any())
} returns selectedAccountName
}
private fun mockRegularAccountCreationFlow() {
// Step 1: Get accounts to know if the current account exists
mockGetAccountsByType(OC_ACCOUNT.type, arrayOf()) // There's no accounts yet
// Step 2: Add new account
every {
accountManager.addAccountExplicitly(any(), any(), any())
} returns true
}
private fun verifyAccountsByTypeAreGot(accountType: String, exactly: Int) {
verify(exactly = exactly) {
accountManager.getAccountsByType(accountType)
}
}
private fun verifyAccountIsExplicitlyAdded(
account: Account,
password: String,
exactly: Int
) {
verify(exactly = exactly) {
accountManager.addAccountExplicitly(
account,
password,
null
)
}
}
private fun verifyAccountInfoIsUpdated(
account: Account,
serverInfo: ServerInfo,
userInfo: UserInfo,
exactly: Int
) {
verify(exactly = exactly) {
// The account info is updated
accountManager.setUserData(account, KEY_OC_ACCOUNT_VERSION, ACCOUNT_VERSION.toString())
accountManager.setUserData(account, KEY_OC_BASE_URL, serverInfo.baseUrl)
accountManager.setUserData(account, KEY_DISPLAY_NAME, userInfo.displayName)
}
}
private fun verifyOAuthParamsAreUpdated(
account: Account,
accessToken: String,
supportsOAuth2: String,
refreshToken: String,
scope: String,
clientInfo: ClientRegistrationInfo,
exactly: Int
) {
verify(exactly = exactly) {
accountManager.setAuthToken(account, OC_AUTH_TOKEN_TYPE, accessToken)
accountManager.setUserData(account, KEY_SUPPORTS_OAUTH2, supportsOAuth2)
accountManager.setUserData(account, KEY_OAUTH2_REFRESH_TOKEN, refreshToken)
accountManager.setUserData(account, KEY_OAUTH2_SCOPE, scope)
accountManager.setUserData(account, KEY_CLIENT_REGISTRATION_CLIENT_SECRET, clientInfo.clientSecret)
accountManager.setUserData(account, KEY_CLIENT_REGISTRATION_CLIENT_ID, clientInfo.clientId)
accountManager.setUserData(account, KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE, clientInfo.clientSecretExpiration.toString())
}
}
private fun verifyUserDataIsGot(account: Account, key: String, exactly: Int) {
verify(exactly = exactly) {
accountManager.getUserData(account, key)
}
}
}
@@ -0,0 +1,151 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.db
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.data.QSferaDatabase
import eu.qsfera.android.data.capabilities.datasources.implementation.OCLocalCapabilitiesDataSource.Companion.toEntity
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
import eu.qsfera.android.testutil.OC_CAPABILITY
import eu.qsfera.android.testutil.livedata.getLastEmittedValue
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@SmallTest
class OCCapabilityDaoTest {
private lateinit var ocCapabilityDao: OCCapabilityDao
private val user1 = "user1@server"
private val user2 = "user2@server"
@Rule
@JvmField
val instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
QSferaDatabase.switchToInMemory(context)
val db: QSferaDatabase = QSferaDatabase.getDatabase(context)
ocCapabilityDao = db.capabilityDao()
}
@Test
fun insertCapabilitiesListAndRead() {
val entityList: List<OCCapabilityEntity> = listOf(
OC_CAPABILITY.copy(accountName = user1).toEntity(),
OC_CAPABILITY.copy(accountName = user2).toEntity()
)
ocCapabilityDao.insertOrReplace(entityList)
val capability = ocCapabilityDao.getCapabilitiesForAccount(user2)
val capabilityAsLiveData = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(user2).getLastEmittedValue()
assertNotNull(capability)
assertNotNull(capabilityAsLiveData)
assertEquals(entityList[1], capability)
assertEquals(entityList[1], capabilityAsLiveData)
}
@Test
fun insertCapabilitiesAndRead() {
val entity1 = OC_CAPABILITY.copy(accountName = user1).toEntity()
val entity2 = OC_CAPABILITY.copy(accountName = user2).toEntity()
ocCapabilityDao.insertOrReplace(entity1)
ocCapabilityDao.insertOrReplace(entity2)
val capability = ocCapabilityDao.getCapabilitiesForAccount(user2)
val capabilityAsLiveData = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(user2).getLastEmittedValue()
assertNotNull(capability)
assertNotNull(capabilityAsLiveData)
assertEquals(entity2, capability)
assertEquals(entity2, capabilityAsLiveData)
}
@Test
fun getNonExistingCapabilities() {
ocCapabilityDao.insertOrReplace(OC_CAPABILITY.copy(accountName = user1).toEntity())
val capability = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(user2).getLastEmittedValue()
assertNull(capability)
}
@Test
fun replaceCapabilityIfAlreadyExists_exists() {
val entity1 = OC_CAPABILITY.copy(filesVersioning = CapabilityBooleanType.FALSE).toEntity()
val entity2 = OC_CAPABILITY.copy(filesVersioning = CapabilityBooleanType.TRUE).toEntity()
ocCapabilityDao.insertOrReplace(entity1)
ocCapabilityDao.replace(listOf(entity2))
val capability = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(OC_CAPABILITY.accountName!!).getLastEmittedValue()
assertNotNull(capability)
assertEquals(entity2, capability)
}
@Test
fun replaceCapabilityIfAlreadyExists_doesNotExist() {
val entity1 = OC_CAPABILITY.copy(accountName = user1).toEntity()
val entity2 = OC_CAPABILITY.copy(accountName = user2).toEntity()
ocCapabilityDao.insertOrReplace(entity1)
ocCapabilityDao.replace(listOf(entity2))
val capability1 = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(user1).getLastEmittedValue()
assertNotNull(capability1)
assertEquals(entity1, capability1)
// capability2 didn't exist before, it should not replace the old one but got created
val capability2 = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(user2).getLastEmittedValue()
assertNotNull(capability2)
assertEquals(entity2, capability2)
}
@Test
fun deleteCapability() {
val entity = OC_CAPABILITY.copy(accountName = user1).toEntity()
ocCapabilityDao.insertOrReplace(entity)
val capability1 = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(user1).getLastEmittedValue()
assertNotNull(capability1)
ocCapabilityDao.deleteByAccountName(user1)
val capability2 = ocCapabilityDao.getCapabilitiesForAccountAsLiveData(user1).getLastEmittedValue()
assertNull(capability2)
}
}
@@ -0,0 +1,74 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import androidx.room.migration.Migration
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.data.QSferaDatabase
import org.junit.Rule
open class MigrationTest {
@Rule
@JvmField
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
QSferaDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
fun getCount(db: SupportSQLiteDatabase, tableName: String): Long =
db.compileStatement("SELECT COUNT(*) FROM `$tableName`").simpleQueryForLong()
fun performMigrationTest(
previousVersion: Int,
currentVersion: Int,
insertData: (SupportSQLiteDatabase) -> Unit,
recoverPreviousData: Boolean = true,
validateMigration: (SupportSQLiteDatabase) -> Unit,
listOfMigrations: Array<Migration>
) {
helper.createDatabase(TEST_DB_NAME, previousVersion).run {
if (recoverPreviousData) insertData(this)
}
helper.runMigrationsAndValidate(
TEST_DB_NAME, currentVersion, true, *listOfMigrations
).also { validateMigration(it) }
}
companion object {
const val TEST_DB_NAME = "migration-test"
const val DB_VERSION_27 = 27
const val DB_VERSION_28 = 28
const val DB_VERSION_29 = 29
const val DB_VERSION_30 = 30
const val DB_VERSION_31 = 31
const val DB_VERSION_32 = 32
const val DB_VERSION_33 = 33
const val DB_VERSION_34 = 34
const val DB_VERSION_35 = 35
const val DB_VERSION_36 = 36
}
}
@@ -0,0 +1,94 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.filters.SmallTest
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.LEGACY_CAPABILITIES_VERSION_MAYOR
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MICRO
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MINOR
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.testutil.OC_CAPABILITY
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Test the migration from database to version 28.
*/
@SmallTest
class MigrationToDB28Test : MigrationTest() {
@Test
fun migrate27To28() {
performMigrationTest(
previousVersion = DB_VERSION_27,
currentVersion = DB_VERSION_28,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo28(database) },
listOfMigrations = arrayOf(MIGRATION_27_28)
)
}
@Test
fun startInVersion28_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_28,
currentVersion = DB_VERSION_28,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo28(database) },
listOfMigrations = arrayOf()
)
}
private fun insertDataToTest(database: SupportSQLiteDatabase) {
database.run {
insert(
CAPABILITIES_TABLE_NAME,
SQLiteDatabase.CONFLICT_NONE,
cvWithDefaultValues
)
close()
}
}
private fun validateMigrationTo28(database: SupportSQLiteDatabase) {
val count = getCount(database, CAPABILITIES_TABLE_NAME)
assertEquals(1, count)
database.close()
}
companion object {
val cvWithDefaultValues = ContentValues().apply {
put(CAPABILITIES_ACCOUNT_NAME, OC_CAPABILITY.accountName)
put(LEGACY_CAPABILITIES_VERSION_MAYOR, OC_CAPABILITY.versionMajor)
put(CAPABILITIES_VERSION_MINOR, OC_CAPABILITY.versionMinor)
put(CAPABILITIES_VERSION_MICRO, OC_CAPABILITY.versionMicro)
put(CAPABILITIES_CORE_POLLINTERVAL, OC_CAPABILITY.corePollInterval)
put(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS, OC_CAPABILITY.filesSharingPublicExpireDateDays)
}
}
}
@@ -0,0 +1,168 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import android.database.sqlite.SQLiteDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.filters.SmallTest
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.data.migrations.MIGRATION_28_29
import eu.qsfera.android.data.migrations.MIGRATION_29_30
import eu.qsfera.android.data.migrations.MIGRATION_30_31
import eu.qsfera.android.data.migrations.MIGRATION_31_32
import eu.qsfera.android.data.migrations.MIGRATION_32_33
import eu.qsfera.android.data.migrations.MIGRATION_33_34
import eu.qsfera.android.data.migrations.MIGRATION_34_35
import eu.qsfera.android.data.migrations.MIGRATION_35_36
import eu.qsfera.android.data.migrations.MIGRATION_37_38
import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.testutil.OC_SHARE
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Test the migration from database to version 29.
*/
@SmallTest
class MigrationToDB29Test : MigrationTest() {
@Test
fun migrationFrom27To29_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_27,
currentVersion = DB_VERSION_29,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo29(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
@Test
fun migrationFrom28To29_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_28,
currentVersion = DB_VERSION_29,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo29(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
@Test
fun startInVersion29_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_29,
currentVersion = DB_VERSION_29,
insertData = { database -> insertDataToTest(database) },
validateMigration = { },
listOfMigrations = arrayOf()
)
}
private fun insertDataToTest(database: SupportSQLiteDatabase) {
database.run {
insert(
CAPABILITIES_TABLE_NAME, SQLiteDatabase.CONFLICT_NONE,
MigrationToDB28Test.cvWithDefaultValues
)
execSQL(
"INSERT INTO `$OCSHARES_TABLE_NAME`" +
"(" +
"share_type, " +
"shate_with, " +
"path, " +
"permissions, " +
"shared_date, " +
"expiration_date, " +
"token, " +
"shared_with_display_name, " +
"share_with_additional_info, " +
"is_directory, " +
"id_remote_shared, " +
"owner_share, " +
"name, " +
"url, " +
"user_id, " +
"item_source, " +
"file_source)" +
" VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf(
OC_SHARE.shareType,
OC_SHARE.shareWith,
OC_SHARE.path,
OC_SHARE.permissions,
OC_SHARE.sharedDate,
OC_SHARE.expirationDate,
OC_SHARE.token,
OC_SHARE.sharedWithDisplayName,
OC_SHARE.sharedWithAdditionalInfo,
OC_SHARE.isFolder,
OC_SHARE.remoteId,
OC_SHARE.accountOwner,
OC_SHARE.name,
OC_SHARE.shareLink,
1,
1,
1
)
)
close()
}
}
private fun validateMigrationTo29(database: SupportSQLiteDatabase) {
val sharesCount = getCount(database, OCSHARES_TABLE_NAME)
assertEquals(0, sharesCount)
val capabilitiesCount = getCount(database, CAPABILITIES_TABLE_NAME)
assertEquals(1, capabilitiesCount)
database.close()
}
}
@@ -0,0 +1,168 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.filters.SmallTest
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_DAV_CHUNKING_VERSION
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.LEGACY_CAPABILITIES_VERSION_MAYOR
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MICRO
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MINOR
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.data.migrations.MIGRATION_28_29
import eu.qsfera.android.data.migrations.MIGRATION_29_30
import eu.qsfera.android.data.migrations.MIGRATION_30_31
import eu.qsfera.android.data.migrations.MIGRATION_31_32
import eu.qsfera.android.data.migrations.MIGRATION_32_33
import eu.qsfera.android.data.migrations.MIGRATION_33_34
import eu.qsfera.android.data.migrations.MIGRATION_34_35
import eu.qsfera.android.data.migrations.MIGRATION_35_36
import eu.qsfera.android.data.migrations.MIGRATION_37_38
import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.testutil.OC_CAPABILITY
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Test the migration from database to version 30.
*/
@SmallTest
class MigrationToDB30Test : MigrationTest() {
@Test
fun migrationFrom27To30_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_27,
currentVersion = DB_VERSION_30,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo30(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
@Test
fun migrationFrom28To30_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_28,
currentVersion = DB_VERSION_30,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo30(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
@Test
fun migrationFrom29To30_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_29,
currentVersion = DB_VERSION_30,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo30(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
@Test
fun startInVersion30_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_30,
currentVersion = DB_VERSION_30,
insertData = { database -> insertDataToTest(database, true) },
validateMigration = { },
listOfMigrations = arrayOf()
)
}
private fun insertDataToTest(database: SupportSQLiteDatabase, addNewField: Boolean = false) {
database.run {
insert(
CAPABILITIES_TABLE_NAME,
SQLiteDatabase.CONFLICT_NONE,
if (addNewField) cvWithDefaultValues else MigrationToDB28Test.cvWithDefaultValues
)
close()
}
}
private fun validateMigrationTo30(database: SupportSQLiteDatabase) {
val capabilitiesCount = getCount(database, CAPABILITIES_TABLE_NAME)
assertEquals(1, capabilitiesCount)
database.close()
}
companion object {
val cvWithDefaultValues = ContentValues().apply {
put(CAPABILITIES_ACCOUNT_NAME, OC_CAPABILITY.accountName)
put(LEGACY_CAPABILITIES_VERSION_MAYOR, OC_CAPABILITY.versionMajor)
put(CAPABILITIES_VERSION_MINOR, OC_CAPABILITY.versionMinor)
put(CAPABILITIES_VERSION_MICRO, OC_CAPABILITY.versionMicro)
put(CAPABILITIES_CORE_POLLINTERVAL, OC_CAPABILITY.corePollInterval)
put(CAPABILITIES_DAV_CHUNKING_VERSION, OC_CAPABILITY.davChunkingVersion)
put(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS, OC_CAPABILITY.filesSharingPublicExpireDateDays)
}
}
}
@@ -0,0 +1,134 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.filters.SmallTest
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.data.migrations.MIGRATION_28_29
import eu.qsfera.android.data.migrations.MIGRATION_29_30
import eu.qsfera.android.data.migrations.MIGRATION_30_31
import eu.qsfera.android.data.migrations.MIGRATION_31_32
import eu.qsfera.android.data.migrations.MIGRATION_32_33
import eu.qsfera.android.data.migrations.MIGRATION_33_34
import eu.qsfera.android.data.migrations.MIGRATION_34_35
import eu.qsfera.android.data.migrations.MIGRATION_35_36
import eu.qsfera.android.data.migrations.MIGRATION_37_38
import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.testutil.OC_SHARE
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Test the migration from database to version 33.
*/
@SmallTest
class MigrationToDB33Test : MigrationTest() {
@Test
fun migrationFrom32to33_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_32,
currentVersion = DB_VERSION_33,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo33(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
@Test
fun startInVersion33_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_33,
currentVersion = DB_VERSION_33,
recoverPreviousData = false,
insertData = { database -> insertDataToTest(database) },
validateMigration = { },
listOfMigrations = arrayOf()
)
}
private fun insertDataToTest(database: SupportSQLiteDatabase) {
database.execSQL(
"INSERT INTO `$OCSHARES_TABLE_NAME`" +
"(" +
"share_type, " +
"shate_with, " +
"path, " +
"permissions, " +
"shared_date, " +
"expiration_date, " +
"token, " +
"shared_with_display_name, " +
"share_with_additional_info, " +
"is_directory, " +
"id_remote_shared, " +
"owner_share, " +
"name, " +
"url, " +
"user_id, " +
"item_source, " +
"file_source)" +
" VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf(
OC_SHARE.shareType,
OC_SHARE.shareWith,
OC_SHARE.path,
OC_SHARE.permissions,
OC_SHARE.sharedDate,
OC_SHARE.expirationDate,
OC_SHARE.token,
OC_SHARE.sharedWithDisplayName,
OC_SHARE.sharedWithAdditionalInfo,
OC_SHARE.isFolder,
OC_SHARE.remoteId,
OC_SHARE.accountOwner,
OC_SHARE.name,
OC_SHARE.shareLink,
1,
1,
1
)
)
}
private fun validateMigrationTo33(database: SupportSQLiteDatabase) {
val sharesCount = getCount(database, OCSHARES_TABLE_NAME)
assertEquals(1, sharesCount)
database.close()
}
}
@@ -0,0 +1,99 @@
/*
* qsfera Android client application
*
* @author Fernando Sanz Velasco
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.filters.SmallTest
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.data.migrations.MIGRATION_28_29
import eu.qsfera.android.data.migrations.MIGRATION_29_30
import eu.qsfera.android.data.migrations.MIGRATION_30_31
import eu.qsfera.android.data.migrations.MIGRATION_31_32
import eu.qsfera.android.data.migrations.MIGRATION_32_33
import eu.qsfera.android.data.migrations.MIGRATION_33_34
import eu.qsfera.android.data.migrations.MIGRATION_34_35
import eu.qsfera.android.data.migrations.MIGRATION_35_36
import eu.qsfera.android.data.migrations.MIGRATION_37_38
import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.testutil.OC_BACKUP
import org.junit.Assert
import org.junit.Test
@SmallTest
class MigrationToDB35Test : MigrationTest() {
@Test
fun migrationFrom34to35_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_34,
currentVersion = DB_VERSION_35,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo35(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
private fun insertDataToTest(database: SupportSQLiteDatabase) {
database.execSQL(
"INSERT INTO `$FOLDER_BACKUP_TABLE_NAME`" +
"(" +
"accountName, " +
"behavior, " +
"sourcePath, " +
"uploadPath, " +
"wifiOnly, " +
"name, " +
"lastSyncTimeStamp)" +
" VALUES " +
"(?, ?, ?, ?, ?, ?, ?)",
arrayOf(
OC_BACKUP.accountName,
OC_BACKUP.behavior,
OC_BACKUP.sourcePath,
OC_BACKUP.uploadPath,
OC_BACKUP.wifiOnly,
OC_BACKUP.name,
OC_BACKUP.lastSyncTimestamp,
)
)
}
private fun validateMigrationTo35(database: SupportSQLiteDatabase) {
val backUp = getCount(database, FOLDER_BACKUP_TABLE_NAME)
Assert.assertEquals(1, backUp)
database.close()
}
}
@@ -0,0 +1,137 @@
/*
* qsfera Android client application
*
* @author Fernando Sanz Velasco
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.filters.SmallTest
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.data.migrations.MIGRATION_28_29
import eu.qsfera.android.data.migrations.MIGRATION_29_30
import eu.qsfera.android.data.migrations.MIGRATION_30_31
import eu.qsfera.android.data.migrations.MIGRATION_31_32
import eu.qsfera.android.data.migrations.MIGRATION_32_33
import eu.qsfera.android.data.migrations.MIGRATION_33_34
import eu.qsfera.android.data.migrations.MIGRATION_34_35
import eu.qsfera.android.data.migrations.MIGRATION_35_36
import eu.qsfera.android.data.migrations.MIGRATION_37_38
import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.testutil.OC_CAPABILITY
import org.junit.Assert
import org.junit.Test
@SmallTest
class MigrationToDB36Test : MigrationTest() {
@Test
fun migrationFrom35to36_containsCorrectData() {
performMigrationTest(
previousVersion = DB_VERSION_35,
currentVersion = DB_VERSION_36,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo36(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
)
)
}
private fun insertDataToTest(database: SupportSQLiteDatabase) {
database.execSQL(
"INSERT INTO `$CAPABILITIES_TABLE_NAME`" +
"(" +
"account, " +
"version_mayor, " +
"version_minor, " +
"version_micro, " +
"version_string, " +
"version_edition, " +
"core_pollinterval, " +
"dav_chunking_version, " +
"sharing_api_enabled, " +
"sharing_public_enabled, " +
"sharing_public_password_enforced, " +
"sharing_public_password_enforced_read_only, " +
"sharing_public_password_enforced_read_write, " +
"sharing_public_password_enforced_public_only, " +
"sharing_public_expire_date_enabled, " +
"sharing_public_expire_date_days, " +
"sharing_public_expire_date_enforced, " +
"sharing_public_upload, " +
"sharing_public_multiple, " +
"supports_upload_only, " +
"sharing_resharing, " +
"sharing_federation_outgoing, " +
"sharing_federation_incoming, " +
"files_bigfilechunking, " +
"files_undelete, " +
"files_versioning)" +
" VALUES " +
"(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
arrayOf(
OC_CAPABILITY.accountName,
OC_CAPABILITY.versionMajor,
OC_CAPABILITY.versionMinor,
OC_CAPABILITY.versionMicro,
OC_CAPABILITY.versionString,
OC_CAPABILITY.versionEdition,
OC_CAPABILITY.corePollInterval,
OC_CAPABILITY.davChunkingVersion,
OC_CAPABILITY.filesSharingApiEnabled,
OC_CAPABILITY.filesSharingPublicEnabled,
OC_CAPABILITY.filesSharingPublicPasswordEnforced,
OC_CAPABILITY.filesSharingPublicPasswordEnforcedReadOnly,
OC_CAPABILITY.filesSharingPublicPasswordEnforcedReadWrite,
OC_CAPABILITY.filesSharingPublicPasswordEnforcedUploadOnly,
OC_CAPABILITY.filesSharingPublicExpireDateEnabled,
OC_CAPABILITY.filesSharingPublicExpireDateDays,
OC_CAPABILITY.filesSharingPublicExpireDateEnforced,
OC_CAPABILITY.filesSharingPublicUpload,
OC_CAPABILITY.filesSharingPublicMultiple,
OC_CAPABILITY.filesSharingPublicSupportsUploadOnly,
OC_CAPABILITY.filesSharingResharing,
OC_CAPABILITY.filesSharingFederationOutgoing,
OC_CAPABILITY.filesSharingFederationIncoming,
OC_CAPABILITY.filesBigFileChunking,
OC_CAPABILITY.filesUndelete,
OC_CAPABILITY.filesVersioning
)
)
}
private fun validateMigrationTo36(database: SupportSQLiteDatabase) {
val capabilityCount = getCount(database, CAPABILITIES_TABLE_NAME)
Assert.assertEquals(1, capabilityCount)
database.close()
}
}
@@ -0,0 +1,125 @@
/*
* qsfera Android client application
*
* Copyright (C) 2024 QSfera.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.roommigrations
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.filters.SmallTest
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.TRANSFERS_TABLE_NAME
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.data.migrations.MIGRATION_28_29
import eu.qsfera.android.data.migrations.MIGRATION_29_30
import eu.qsfera.android.data.migrations.MIGRATION_30_31
import eu.qsfera.android.data.migrations.MIGRATION_31_32
import eu.qsfera.android.data.migrations.MIGRATION_32_33
import eu.qsfera.android.data.migrations.MIGRATION_33_34
import eu.qsfera.android.data.migrations.MIGRATION_34_35
import eu.qsfera.android.data.migrations.MIGRATION_35_36
import eu.qsfera.android.data.migrations.MIGRATION_37_38
import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.data.migrations.MIGRATION_47_48
import org.junit.Assert
import org.junit.Test
@SmallTest
class MigrationToDB48Test : MigrationTest() {
@Test
fun migrationFrom47to48_containsCorrectData() {
performMigrationTest(
previousVersion = 47,
currentVersion = 48,
insertData = { database -> insertDataToTest(database) },
validateMigration = { database -> validateMigrationTo48(database) },
listOfMigrations = arrayOf(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
MIGRATION_47_48
)
)
}
private fun insertDataToTest(database: SupportSQLiteDatabase) {
database.execSQL(
"INSERT INTO `$TRANSFERS_TABLE_NAME`" +
"(" +
"localPath, " +
"remotePath, " +
"accountName, " +
"fileSize, " +
"status, " +
"localBehaviour, " +
"forceOverwrite, " +
"createdBy" +
")" +
" VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf(
"/storage/emulated/0/test.txt",
"/test.txt",
"user@example.com",
1024,
0,
0,
0,
0
)
)
}
private fun validateMigrationTo48(database: SupportSQLiteDatabase) {
val cursor = database.query("SELECT * FROM $TRANSFERS_TABLE_NAME")
Assert.assertTrue(cursor.moveToFirst())
// Check if new columns exist
val tusUploadUrlIndex = cursor.getColumnIndex("tusUploadUrl")
val tusUploadLengthIndex = cursor.getColumnIndex("tusUploadLength")
val tusUploadMetadataIndex = cursor.getColumnIndex("tusUploadMetadata")
val tusUploadChecksumIndex = cursor.getColumnIndex("tusUploadChecksum")
val tusResumableVersionIndex = cursor.getColumnIndex("tusResumableVersion")
val tusUploadExpiresIndex = cursor.getColumnIndex("tusUploadExpires")
val tusUploadConcatIndex = cursor.getColumnIndex("tusUploadConcat")
Assert.assertTrue(tusUploadUrlIndex != -1)
Assert.assertTrue(tusUploadLengthIndex != -1)
Assert.assertTrue(tusUploadMetadataIndex != -1)
Assert.assertTrue(tusUploadChecksumIndex != -1)
Assert.assertTrue(tusResumableVersionIndex != -1)
Assert.assertTrue(tusUploadExpiresIndex != -1)
Assert.assertTrue(tusUploadConcatIndex != -1)
// Check if existing data is preserved
val localPathIndex = cursor.getColumnIndex("localPath")
Assert.assertEquals("/storage/emulated/0/test.txt", cursor.getString(localPathIndex))
cursor.close()
database.close()
}
}
@@ -0,0 +1,521 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.sharing.shares.db
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.data.QSferaDatabase
import eu.qsfera.android.data.sharing.shares.datasources.implementation.OCLocalShareDataSource.Companion.toEntity
import eu.qsfera.android.domain.sharing.shares.model.ShareType
import eu.qsfera.android.testutil.OC_PRIVATE_SHARE
import eu.qsfera.android.testutil.OC_PUBLIC_SHARE
import eu.qsfera.android.testutil.livedata.getLastEmittedValue
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@MediumTest
class OCShareDaoTest {
@Rule
@JvmField
val instantExecutorRule = InstantTaskExecutorRule()
private lateinit var ocShareDao: OCShareDao
private val privateShareTypeValues = listOf(
ShareType.USER.value, ShareType.GROUP.value, ShareType.FEDERATED.value
)
@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
QSferaDatabase.switchToInMemory(context)
val db: QSferaDatabase = QSferaDatabase.getDatabase(context)
ocShareDao = db.shareDao()
}
/******************************************************************************************************
*********************************************** COMMON ***********************************************
******************************************************************************************************/
@Test
fun insertEmptySharesList() {
ocShareDao.insertOrReplace(listOf())
val shares = ocShareDao.getSharesAsLiveData(
"/Test/",
"admin@server",
mutableListOf(ShareType.PUBLIC_LINK.value).plus(privateShareTypeValues)
).getLastEmittedValue()!!
assertNotNull(shares)
assertEquals(0, shares.size)
}
@Test
fun insertSharesFromDifferentFilesAndRead() {
ocShareDao.insertOrReplace(
listOf(
OC_PUBLIC_SHARE.copy(
path = "/Photos/",
isFolder = true,
name = "Photos folder link",
shareLink = "http://server:port/s/1",
accountOwner = "admin@server"
).toEntity(),
OC_PUBLIC_SHARE.copy(
path = "/Photos/image1.jpg",
isFolder = false,
name = "Image 1 link",
shareLink = "http://server:port/s/2",
accountOwner = "admin@server"
).toEntity(),
OC_PRIVATE_SHARE.copy(
path = "/Photos/image2.jpg",
isFolder = false,
shareWith = "username",
sharedWithDisplayName = "John",
accountOwner = "admin@server"
).toEntity()
)
)
val photosFolderPublicShares = ocShareDao.getSharesAsLiveData(
"/Photos/",
"admin@server",
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(photosFolderPublicShares)
assertEquals(1, photosFolderPublicShares.size)
assertEquals("/Photos/", photosFolderPublicShares[0].path)
assertEquals(true, photosFolderPublicShares[0].isFolder)
assertEquals("admin@server", photosFolderPublicShares[0].accountOwner)
assertEquals("Photos folder link", photosFolderPublicShares[0].name)
assertEquals("http://server:port/s/1", photosFolderPublicShares[0].shareLink)
val image1PublicShares = ocShareDao.getSharesAsLiveData(
"/Photos/image1.jpg",
"admin@server",
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(image1PublicShares)
assertEquals(1, image1PublicShares.size)
assertEquals("/Photos/image1.jpg", image1PublicShares[0].path)
assertEquals(false, image1PublicShares[0].isFolder)
assertEquals("admin@server", image1PublicShares[0].accountOwner)
assertEquals("Image 1 link", image1PublicShares[0].name)
assertEquals("http://server:port/s/2", image1PublicShares[0].shareLink)
val image2PrivateShares =
ocShareDao.getSharesAsLiveData(
"/Photos/image2.jpg", "admin@server", privateShareTypeValues
).getLastEmittedValue()!!
assertNotNull(image2PrivateShares)
assertEquals(1, image2PrivateShares.size)
assertEquals("/Photos/image2.jpg", image2PrivateShares[0].path)
assertEquals(false, image2PrivateShares[0].isFolder)
assertEquals("admin@server", image1PublicShares[0].accountOwner)
assertEquals("username", image2PrivateShares[0].shareWith)
assertEquals("John", image2PrivateShares[0].sharedWithDisplayName)
}
@Test
fun insertSharesFromDifferentAccountsAndRead() {
ocShareDao.insertOrReplace(
listOf(
OC_PUBLIC_SHARE.copy(
path = "/Documents/document1.docx",
isFolder = false,
accountOwner = "user1@server",
name = "Document 1 link",
shareLink = "http://server:port/s/1"
).toEntity(),
OC_PUBLIC_SHARE.copy(
path = "/Documents/document1.docx",
isFolder = false,
accountOwner = "user2@server",
name = "Document 1 link",
shareLink = "http://server:port/s/2"
).toEntity(),
OC_PRIVATE_SHARE.copy(
path = "/Documents/document1.docx",
isFolder = false,
accountOwner = "user3@server",
shareWith = "user_name",
sharedWithDisplayName = "Patrick"
).toEntity()
)
)
val document1PublicSharesForUser1 = ocShareDao.getSharesAsLiveData(
"/Documents/document1.docx",
"user1@server",
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(document1PublicSharesForUser1)
assertEquals(1, document1PublicSharesForUser1.size)
assertEquals("/Documents/document1.docx", document1PublicSharesForUser1[0].path)
assertEquals(false, document1PublicSharesForUser1[0].isFolder)
assertEquals("user1@server", document1PublicSharesForUser1[0].accountOwner)
assertEquals("Document 1 link", document1PublicSharesForUser1[0].name)
assertEquals("http://server:port/s/1", document1PublicSharesForUser1[0].shareLink)
val document1PublicSharesForUser2 = ocShareDao.getSharesAsLiveData(
"/Documents/document1.docx",
"user2@server",
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(document1PublicSharesForUser2)
assertEquals(1, document1PublicSharesForUser2.size)
assertEquals("/Documents/document1.docx", document1PublicSharesForUser2[0].path)
assertEquals(false, document1PublicSharesForUser2[0].isFolder)
assertEquals("user2@server", document1PublicSharesForUser2[0].accountOwner)
assertEquals("Document 1 link", document1PublicSharesForUser2[0].name)
assertEquals("http://server:port/s/2", document1PublicSharesForUser2[0].shareLink)
val document1PrivateSharesForUser3 = ocShareDao.getSharesAsLiveData(
"/Documents/document1.docx",
"user3@server",
privateShareTypeValues
).getLastEmittedValue()!!
assertNotNull(document1PrivateSharesForUser3)
assertEquals(1, document1PrivateSharesForUser3.size)
assertEquals("/Documents/document1.docx", document1PrivateSharesForUser3[0].path)
assertEquals(false, document1PrivateSharesForUser3[0].isFolder)
assertEquals("user3@server", document1PrivateSharesForUser3[0].accountOwner)
assertEquals("user_name", document1PrivateSharesForUser3[0].shareWith)
assertEquals("Patrick", document1PrivateSharesForUser3[0].sharedWithDisplayName)
}
@Test
fun testAutogenerateId() {
ocShareDao.insertOrReplace(
listOf(
OC_PUBLIC_SHARE.copy(
path = "/Documents/document1.docx",
accountOwner = "user1@server",
name = "Document 1 link",
shareLink = "http://server:port/s/1"
).toEntity(),
OC_PUBLIC_SHARE.copy(
path = "/Documents/document1.docx",
accountOwner = "user1@server",
name = "Document 1 link",
shareLink = "http://server:port/s/1"
).toEntity()
)
)
val sharesWithSameValues = ocShareDao.getSharesAsLiveData(
"/Documents/document1.docx",
"user1@server",
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(sharesWithSameValues)
assertEquals(2, sharesWithSameValues.size)
assertEquals("/Documents/document1.docx", sharesWithSameValues[0].path)
assertEquals("/Documents/document1.docx", sharesWithSameValues[1].path)
assertEquals(false, sharesWithSameValues[0].isFolder)
assertEquals(false, sharesWithSameValues[1].isFolder)
assertEquals("user1@server", sharesWithSameValues[0].accountOwner)
assertEquals("user1@server", sharesWithSameValues[1].accountOwner)
assertEquals("Document 1 link", sharesWithSameValues[0].name)
assertEquals("Document 1 link", sharesWithSameValues[1].name)
assertEquals("http://server:port/s/1", sharesWithSameValues[0].shareLink)
assertEquals("http://server:port/s/1", sharesWithSameValues[1].shareLink)
assertNotNull(sharesWithSameValues[0].id)
assertNotNull(sharesWithSameValues[1].id)
assert(sharesWithSameValues[0].id != sharesWithSameValues[1].id)
}
/******************************************************************************************************
******************************************* PRIVATE SHARES *******************************************
******************************************************************************************************/
@Test
fun getNonExistingPrivateShare() {
val privateShare = createDefaultPrivateShareEntity()
ocShareDao.insertOrReplace(privateShare)
val nonExistingPrivateShare = ocShareDao.getSharesAsLiveData(
privateShare.path,
"user@server",
privateShareTypeValues
).getLastEmittedValue()!!
assertNotNull(nonExistingPrivateShare)
assertEquals(0, nonExistingPrivateShare.size)
}
@Test
fun replacePrivateShareIfAlreadyExists_exists() {
val privateShare = createDefaultPrivateShareEntity()
ocShareDao.insertOrReplace(createDefaultPrivateShareEntity())
val privateShareToReplace = createDefaultPrivateShareEntity(shareWith = "userName")
ocShareDao.replaceShares(
listOf(privateShareToReplace)
)
val textShares = ocShareDao.getSharesAsLiveData(
privateShare.path,
privateShare.accountOwner,
listOf(ShareType.USER.value)
).getLastEmittedValue()!!
assertNotNull(textShares)
assertEquals(1, textShares.size)
assertEquals(privateShareToReplace.shareWith, textShares[0].shareWith)
}
@Test
fun replacePrivateShareIfAlreadyExists_doesNotExist() {
val privateShare = createDefaultPrivateShareEntity(
shareType = ShareType.GROUP
)
ocShareDao.insertOrReplace(privateShare)
val privateShareToReplace = createDefaultPrivateShareEntity(
shareType = ShareType.GROUP,
shareWith = "userName",
path = "/Texts/text2.txt"
)
ocShareDao.replaceShares(
listOf(privateShareToReplace)
)
val text1Shares = ocShareDao.getSharesAsLiveData(
privateShare.path,
privateShare.accountOwner,
listOf(ShareType.GROUP.value)
).getLastEmittedValue()!!
assertNotNull(text1Shares)
assertEquals(1, text1Shares.size)
assertEquals(privateShare.shareWith, text1Shares[0].shareWith)
// text2 link didn't exist before, it should not replace the old one but be created
val text2Shares = ocShareDao.getSharesAsLiveData(
privateShareToReplace.path,
privateShareToReplace.accountOwner,
listOf(ShareType.GROUP.value)
).getLastEmittedValue()!!
assertNotNull(text2Shares)
assertEquals(1, text2Shares.size)
assertEquals(privateShareToReplace.shareWith, text2Shares[0].shareWith)
}
@Test
fun updatePrivateShare() {
val privateShare = createDefaultPrivateShareEntity()
ocShareDao.insertOrReplace(privateShare)
ocShareDao.update(
createDefaultPrivateShareEntity(permissions = 17)
)
val textShares = ocShareDao.getSharesAsLiveData(
privateShare.path,
privateShare.accountOwner,
listOf(ShareType.USER.value)
).getLastEmittedValue()!!
assertNotNull(textShares)
assertEquals(1, textShares.size)
assertEquals(17, textShares[0].permissions)
}
@Test
fun deletePrivateShare() {
val privateShare = createDefaultPrivateShareEntity()
ocShareDao.insertOrReplace(createDefaultPrivateShareEntity())
ocShareDao.deleteShare(privateShare.remoteId)
val textShares = ocShareDao.getSharesAsLiveData(
privateShare.path,
privateShare.accountOwner,
listOf(ShareType.USER.value)
).getLastEmittedValue()!!
assertNotNull(textShares)
assertEquals(0, textShares.size) // List of textShares empty after deleting the existing share
}
private fun createDefaultPrivateShareEntity(
shareType: ShareType = ShareType.USER,
shareWith: String = "username",
path: String = "/Texts/text1.txt",
permissions: Int = -1,
shareWithDisplayName: String = "Steve"
) = OC_PRIVATE_SHARE.copy(
shareType = shareType,
shareWith = shareWith,
path = path,
permissions = permissions,
isFolder = false,
sharedWithDisplayName = shareWithDisplayName
).toEntity()
/******************************************************************************************************
******************************************* PUBLIC SHARES ********************************************
******************************************************************************************************/
@Test
fun getNonExistingPublicShare() {
val publicShare = createDefaultPublicShareEntity()
ocShareDao.insertOrReplace(publicShare)
val nonExistingPublicShare = ocShareDao.getSharesAsLiveData(
publicShare.path,
"user@server",
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(nonExistingPublicShare)
assertEquals(0, nonExistingPublicShare.size)
}
@Test
fun replacePublicShareIfAlreadyExists_exists() {
val publicShare = createDefaultPublicShareEntity()
ocShareDao.insertOrReplace(publicShare)
val publicShareToReplace = createDefaultPublicShareEntity(name = "Text 2 link")
ocShareDao.replaceShares(
listOf(publicShareToReplace)
)
val textShares = ocShareDao.getSharesAsLiveData(
publicShare.path,
publicShare.accountOwner,
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(textShares)
assertEquals(1, textShares.size)
assertEquals(publicShareToReplace.name, textShares[0].name)
}
@Test
fun replacePublicShareIfAlreadyExists_doesNotExist() {
val publicShare = createDefaultPublicShareEntity()
ocShareDao.insertOrReplace(publicShare)
val publicShareToReplace = createDefaultPublicShareEntity(path = "/Texts/text2.txt", name = "Text 2 link")
ocShareDao.replaceShares(
listOf(publicShareToReplace)
)
val text1Shares = ocShareDao.getSharesAsLiveData(
publicShare.path,
publicShare.accountOwner,
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(text1Shares)
assertEquals(1, text1Shares.size)
assertEquals(publicShare.name, text1Shares[0].name)
// text2 link didn't exist before, it should not replace the old one but be created
val text2Shares = ocShareDao.getSharesAsLiveData(
publicShareToReplace.path,
publicShareToReplace.accountOwner,
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(text2Shares)
assertEquals(1, text2Shares.size)
assertEquals(publicShareToReplace.name, text2Shares[0].name)
}
@Test
fun updatePublicShare() {
val publicShare = createDefaultPublicShareEntity()
ocShareDao.insertOrReplace(publicShare)
val publicShareToUpdate = createDefaultPublicShareEntity(name = "Text 1 link updated", expirationDate = 2000)
ocShareDao.update(publicShareToUpdate)
val textShares = ocShareDao.getSharesAsLiveData(
publicShareToUpdate.path,
publicShareToUpdate.accountOwner,
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(textShares)
assertEquals(1, textShares.size)
assertEquals(publicShareToUpdate.name, textShares[0].name)
assertEquals(publicShareToUpdate.expirationDate, textShares[0].expirationDate)
}
@Test
fun deletePublicShare() {
val publicShare = createDefaultPublicShareEntity()
ocShareDao.insertOrReplace(publicShare)
ocShareDao.deleteShare(publicShare.remoteId)
val textShares = ocShareDao.getSharesAsLiveData(
publicShare.path,
publicShare.accountOwner,
listOf(ShareType.PUBLIC_LINK.value)
).getLastEmittedValue()!!
assertNotNull(textShares)
assertEquals(0, textShares.size) // List of textShares empty after deleting the existing share
}
private fun createDefaultPublicShareEntity(
path: String = "/Texts/text1.txt",
expirationDate: Long = 1000,
name: String = "Text 1 link"
) = OC_PUBLIC_SHARE.copy(
path = path,
expirationDate = expirationDate,
isFolder = false,
name = name,
shareLink = "http://server:port/s/1"
).toEntity()
}
@@ -0,0 +1,182 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.spaces.db
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.data.QSferaDatabase
import eu.qsfera.android.data.spaces.datasources.implementation.OCLocalSpacesDataSource.Companion.toEntity
import eu.qsfera.android.data.spaces.datasources.implementation.OCLocalSpacesDataSource.Companion.toModel
import eu.qsfera.android.domain.spaces.model.OCSpace.Companion.DRIVE_TYPE_PROJECT
import eu.qsfera.android.testutil.OC_SPACE_PROJECT_WITHOUT_IMAGE
import eu.qsfera.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE
import eu.qsfera.android.testutil.OC_SPACE_SPECIAL_IMAGE
import eu.qsfera.android.testutil.OC_SPACE_SPECIAL_README
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
@MediumTest
class SpacesDaoTest {
@Rule
@JvmField
val instantExecutorRule = InstantTaskExecutorRule()
private lateinit var spacesDao: SpacesDao
@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
QSferaDatabase.switchToInMemory(context)
val db: QSferaDatabase = QSferaDatabase.getDatabase(context)
spacesDao = db.spacesDao()
}
@Test
fun insertOrDeleteSpacesWithEmptyDatabase() = runTest {
val accountName = OC_SPACE_PROJECT_WITHOUT_IMAGE.accountName
val specialsToInsert = listOf(
OC_SPACE_SPECIAL_README.toEntity(accountName, OC_SPACE_PROJECT_WITHOUT_IMAGE.id)
)
val spacesToInsertModel = listOf(
OC_SPACE_PROJECT_WITHOUT_IMAGE
)
val spacesToInsert = spacesToInsertModel.map { it.toEntity() }
spacesDao.insertOrDeleteSpaces(spacesToInsert, specialsToInsert)
val spacesInDatabase = spacesDao.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
accountName = accountName,
filterDriveTypes = setOf(DRIVE_TYPE_PROJECT),
).first().map { it.toModel() }
assertNotNull(spacesInDatabase)
assertEquals(1, spacesInDatabase.size)
assertEquals(spacesToInsertModel, spacesInDatabase)
}
@Test
fun insertOrDeleteSpacesWithSpacesAlreadyInDatabaseNotAttachedToAccountAnymore() = runTest {
val accountName = OC_SPACE_PROJECT_WITHOUT_IMAGE.accountName
val specialsAlreadyInDatabaseToInsert = listOf(
OC_SPACE_SPECIAL_IMAGE.toEntity(accountName, OC_SPACE_PROJECT_WITH_IMAGE.id),
OC_SPACE_SPECIAL_README.toEntity(accountName, OC_SPACE_PROJECT_WITH_IMAGE.id)
)
val spacesAlreadyInDatabaseToInsert = listOf(
OC_SPACE_PROJECT_WITH_IMAGE.toEntity()
)
spacesDao.insertOrDeleteSpaces(spacesAlreadyInDatabaseToInsert, specialsAlreadyInDatabaseToInsert)
val spacesAlreadyInDatabase = spacesDao.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
accountName = accountName,
filterDriveTypes = setOf(DRIVE_TYPE_PROJECT),
).first()
assertEquals(1, spacesAlreadyInDatabase.size)
assertEquals(2, spacesAlreadyInDatabase[0].specials.size)
val newSpecialsToInsert = listOf(
OC_SPACE_SPECIAL_README.toEntity(accountName, OC_SPACE_PROJECT_WITHOUT_IMAGE.id),
)
val newSpacesToInsertModel = listOf(
OC_SPACE_PROJECT_WITHOUT_IMAGE
)
val newSpacesToInsert = newSpacesToInsertModel.map { it.toEntity() }
spacesDao.insertOrDeleteSpaces(newSpacesToInsert, newSpecialsToInsert)
val spacesInDatabaseEntity = spacesDao.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
accountName = accountName,
filterDriveTypes = setOf(DRIVE_TYPE_PROJECT),
).first()
val spacesInDatabase = spacesInDatabaseEntity.map { it.toModel() }
val specialsInDatabase = spacesInDatabaseEntity.flatMap { it.specials }
assertNotNull(spacesInDatabase)
assertEquals(1, spacesInDatabase.size)
assertEquals(1, specialsInDatabase.size)
assertEquals(newSpacesToInsertModel, spacesInDatabase)
}
@Test
fun insertOrDeleteSpacesWithSpacesAlreadyInDatabaseStillAttachedToAccount() = runTest {
val accountName = OC_SPACE_PROJECT_WITHOUT_IMAGE.accountName
val specialsAlreadyInDatabaseToInsert = listOf(
OC_SPACE_SPECIAL_IMAGE.toEntity(accountName, OC_SPACE_PROJECT_WITH_IMAGE.id),
OC_SPACE_SPECIAL_README.toEntity(accountName, OC_SPACE_PROJECT_WITH_IMAGE.id)
)
val spacesAlreadyInDatabaseToInsert = listOf(
OC_SPACE_PROJECT_WITH_IMAGE.toEntity()
)
spacesDao.insertOrDeleteSpaces(spacesAlreadyInDatabaseToInsert, specialsAlreadyInDatabaseToInsert)
val spacesAlreadyInDatabase = spacesDao.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
accountName = accountName,
filterDriveTypes = setOf(DRIVE_TYPE_PROJECT),
).first()
assertEquals(1, spacesAlreadyInDatabase.size)
assertEquals(2, spacesAlreadyInDatabase[0].specials.size)
val anotherSpaceId = "8871f4f3-fc6f-4a66-8bed-62f175f76f38$0aa0e03c-ec36-498c-bb9f-857315568190"
val newSpecialsToInsert = listOf(
OC_SPACE_SPECIAL_README.copy(
id = "$anotherSpaceId!1c7bbc13-469f-482c-8f13-55ae1402b4c4"
).toEntity(accountName, OC_SPACE_PROJECT_WITHOUT_IMAGE.id),
) + specialsAlreadyInDatabaseToInsert
val newSpacesToInsert = listOf(
OC_SPACE_PROJECT_WITHOUT_IMAGE.toEntity()
) + spacesAlreadyInDatabaseToInsert
spacesDao.insertOrDeleteSpaces(newSpacesToInsert, newSpecialsToInsert)
val spacesInDatabase = spacesDao.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
accountName = accountName,
filterDriveTypes = setOf(DRIVE_TYPE_PROJECT),
).first()
val specialsInDatabase = spacesInDatabase.flatMap { it.specials }
assertNotNull(spacesInDatabase)
assertEquals(2, spacesInDatabase.size)
assertEquals(3, specialsInDatabase.size)
}
}
@@ -0,0 +1,86 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.user
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.data.QSferaDatabase
import eu.qsfera.android.data.user.datasources.implementation.OCLocalUserDataSource.Companion.toEntity
import eu.qsfera.android.data.user.datasources.implementation.OCLocalUserDataSource.Companion.toModel
import eu.qsfera.android.data.user.db.UserDao
import eu.qsfera.android.testutil.OC_ACCOUNT_NAME
import eu.qsfera.android.testutil.OC_USER_QUOTA
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@MediumTest
class UserDaoTest {
@Rule
@JvmField
val instantExecutorRule = InstantTaskExecutorRule()
private lateinit var userDao: UserDao
private val userQuotaEntity = OC_USER_QUOTA.toEntity()
@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
QSferaDatabase.switchToInMemory(context)
val db: QSferaDatabase = QSferaDatabase.getDatabase(context)
userDao = db.userDao()
}
/******************************************************************************************************
******************************************** User Quota **********************************************
******************************************************************************************************/
@Test
fun insertQuotaForAccount() {
userDao.insertOrReplace(userQuotaEntity)
val userQuotaEntity = userDao.getQuotaForAccount(OC_ACCOUNT_NAME)
assertNotNull(userQuotaEntity)
assertEquals(OC_USER_QUOTA, userQuotaEntity?.toModel())
}
@Test
fun replaceQuotaForAccount() {
userDao.insertOrReplace(userQuotaEntity)
userDao.insertOrReplace(userQuotaEntity.copy(available = -3))
val userQuotaEntity = userDao.getQuotaForAccount(OC_ACCOUNT_NAME)
assertNotNull(userQuotaEntity)
assertEquals(OC_USER_QUOTA.copy(available = -3), userQuotaEntity?.toModel())
}
@Test
fun getQuotaForAccountNull() {
val userQuotaEntity = userDao.getQuotaForAccount(OC_ACCOUNT_NAME)
assertNull(userQuotaEntity)
}
}
@@ -0,0 +1 @@
<manifest />
@@ -0,0 +1,167 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import androidx.core.net.toUri
import eu.qsfera.android.data.authentication.SELECTED_ACCOUNT
import eu.qsfera.android.data.providers.SharedPreferencesProvider
import eu.qsfera.android.lib.common.ConnectionValidator
import eu.qsfera.android.lib.common.QSferaAccount
import eu.qsfera.android.lib.common.QSferaClient
import eu.qsfera.android.lib.common.SingleSessionManager
import eu.qsfera.android.lib.common.authentication.QSferaCredentials
import eu.qsfera.android.lib.common.authentication.QSferaCredentialsFactory.getAnonymousCredentials
import eu.qsfera.android.lib.resources.appregistry.services.AppRegistryService
import eu.qsfera.android.lib.resources.appregistry.services.OCAppRegistryService
import eu.qsfera.android.lib.resources.files.services.FileService
import eu.qsfera.android.lib.resources.files.services.implementation.OCFileService
import eu.qsfera.android.lib.resources.shares.services.ShareService
import eu.qsfera.android.lib.resources.shares.services.ShareeService
import eu.qsfera.android.lib.resources.shares.services.implementation.OCShareService
import eu.qsfera.android.lib.resources.shares.services.implementation.OCShareeService
import eu.qsfera.android.lib.resources.spaces.services.OCSpacesService
import eu.qsfera.android.lib.resources.spaces.services.SpacesService
import eu.qsfera.android.lib.resources.status.services.CapabilityService
import eu.qsfera.android.lib.resources.status.services.implementation.OCCapabilityService
import eu.qsfera.android.lib.resources.users.services.UserService
import eu.qsfera.android.lib.resources.users.services.implementation.OCUserService
import timber.log.Timber
class ClientManager(
private val accountManager: AccountManager,
private val preferencesProvider: SharedPreferencesProvider,
val context: Context,
val accountType: String,
private val connectionValidator: ConnectionValidator
) {
// This client will maintain cookies across the whole login process.
private var qsferaClient: QSferaClient? = null
// Cached client to avoid retrieving the client for each service
private var qsferaClientForCurrentAccount: QSferaClient? = null
init {
SingleSessionManager.setConnectionValidator(connectionValidator)
}
/**
* Returns a client for the login process.
* Helpful to keep the cookies from the status request to the final login and user info retrieval.
* For regular uses, use [getClientForAccount]
*/
fun getClientForAnonymousCredentials(
path: String,
requiresNewClient: Boolean,
qsferaCredentials: QSferaCredentials? = getAnonymousCredentials()
): QSferaClient {
val safeClient = qsferaClient
val pathUri = path.toUri()
return if (requiresNewClient || safeClient == null || safeClient.baseUri != pathUri) {
Timber.d("Creating new client for path: $pathUri. Old client path: ${safeClient?.baseUri}, requiresNewClient: $requiresNewClient")
QSferaClient(
pathUri,
connectionValidator,
true,
SingleSessionManager.getDefaultSingleton(),
context
).apply {
credentials = qsferaCredentials
}.also {
qsferaClient = it
}
} else {
Timber.d("Reusing anonymous client for ${safeClient.baseUri}")
safeClient.apply {
credentials = qsferaCredentials
}
}
}
private fun getClientForAccount(
accountName: String?
): QSferaClient {
val account: Account? = if (accountName.isNullOrBlank()) {
getCurrentAccount()
} else {
accountManager.getAccountsByType(accountType).firstOrNull { it.name == accountName }
}
val qsferaAccount = QSferaAccount(account, context)
return SingleSessionManager.getDefaultSingleton().getClientFor(qsferaAccount, context, connectionValidator).also {
qsferaClientForCurrentAccount = it
}
}
private fun getCurrentAccount(): Account? {
val ocAccounts = accountManager.getAccountsByType(accountType)
val accountName = preferencesProvider.getString(SELECTED_ACCOUNT, null)
// account validation: the saved account MUST be in the list of qsfera Accounts known by the AccountManager
accountName?.let { selectedAccountName ->
ocAccounts.firstOrNull { it.name == selectedAccountName }?.let { return it }
}
// take first account as fallback
return ocAccounts.firstOrNull()
}
fun getClientForCoilThumbnails(accountName: String) = getClientForAccount(accountName = accountName)
fun getUserService(accountName: String? = ""): UserService {
val qsferaClient = getClientForAccount(accountName)
return OCUserService(client = qsferaClient)
}
fun getFileService(accountName: String? = ""): FileService {
val qsferaClient = getClientForAccount(accountName)
return OCFileService(client = qsferaClient)
}
fun getCapabilityService(accountName: String? = ""): CapabilityService {
val qsferaClient = getClientForAccount(accountName)
return OCCapabilityService(client = qsferaClient)
}
fun getShareService(accountName: String? = ""): ShareService {
val qsferaClient = getClientForAccount(accountName)
return OCShareService(client = qsferaClient)
}
fun getShareeService(accountName: String? = ""): ShareeService {
val qsferaClient = getClientForAccount(accountName)
return OCShareeService(client = qsferaClient)
}
fun getSpacesService(accountName: String): SpacesService {
val qsferaClient = getClientForAccount(accountName)
return OCSpacesService(client = qsferaClient)
}
fun getAppRegistryService(accountName: String): AppRegistryService {
val qsferaClient = getClientForAccount(accountName)
return OCAppRegistryService(client = qsferaClient)
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.qsfera.android.data
import android.os.Handler
import android.os.Looper
import java.util.concurrent.Executor
import java.util.concurrent.Executors
/**
* Global executor pools for the whole application.
*
* Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind
* webservice requests).
*/
open class Executors(
private val diskIO: Executor,
private val networkIO: Executor,
private val mainThread: Executor
) {
constructor() : this(
Executors.newSingleThreadExecutor(),
Executors.newFixedThreadPool(3),
MainThreadExecutor()
)
fun diskIO(): Executor = diskIO
fun networkIO(): Executor = networkIO
fun mainThread(): Executor = mainThread
private class MainThreadExecutor : Executor {
private val mainThreadHandler = Handler(Looper.getMainLooper())
override fun execute(command: Runnable) {
mainThreadHandler.post(command)
}
}
}
@@ -0,0 +1,150 @@
/**
* qsfera Android client application
*
* @author Bartek Przybylski
* @author David A. Velasco
* @author masensio
* @author David González Verdugo
* Copyright (C) 2011 Bartek Przybylski
* Copyright (C) 2020 ownCloud GmbH.
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data;
import android.provider.BaseColumns;
/**
* Meta-Class that holds various static field information
*/
public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final String NEW_DB_NAME = "qsfera_database";
public static final int DB_VERSION = 49;
private ProviderMeta() {
}
static public class ProviderTableMeta implements BaseColumns {
public static final String CAPABILITIES_TABLE_NAME = "capabilities";
public static final String FILES_SYNC_TABLE_NAME = "files_sync";
public static final String FILES_TABLE_NAME = "files";
public static final String FOLDER_BACKUP_TABLE_NAME = "folder_backup";
public static final String OCSHARES_TABLE_NAME = "ocshares";
public static final String SPACES_TABLE_NAME = "spaces";
public static final String SPACES_SPECIAL_TABLE_NAME = "spaces_special";
public static final String TRANSFERS_TABLE_NAME = "transfers";
public static final String USER_QUOTAS_TABLE_NAME = "user_quotas";
public static final String APP_REGISTRY_TABLE_NAME = "app_registry";
// Columns of ocshares table
public static final String OCSHARES_ACCOUNT_OWNER = "owner_share";
public static final String OCSHARES_EXPIRATION_DATE = "expiration_date";
public static final String OCSHARES_ID_REMOTE_SHARED = "id_remote_shared";
public static final String OCSHARES_IS_DIRECTORY = "is_directory";
public static final String OCSHARES_NAME = "name";
public static final String OCSHARES_PATH = "path";
public static final String OCSHARES_PERMISSIONS = "permissions";
public static final String OCSHARES_SHARED_DATE = "shared_date";
public static final String OCSHARES_SHARE_TYPE = "share_type";
public static final String OCSHARES_SHARE_WITH = "share_with";
public static final String OCSHARES_SHARE_WITH_ADDITIONAL_INFO = "share_with_additional_info";
public static final String OCSHARES_SHARE_WITH_DISPLAY_NAME = "shared_with_display_name";
public static final String OCSHARES_TOKEN = "token";
public static final String OCSHARES_URL = "url";
// Columns of capabilities table
public static final String CAPABILITIES_ACCOUNT_NAME = "account";
public static final String CAPABILITIES_APP_PROVIDERS_PREFIX = "app_providers_";
public static final String CAPABILITIES_CORE_POLLINTERVAL = "core_pollinterval";
public static final String CAPABILITIES_DAV_CHUNKING_VERSION = "dav_chunking_version";
public static final String CAPABILITIES_FILES_BIGFILECHUNKING = "files_bigfilechunking";
public static final String CAPABILITIES_FILES_PRIVATE_LINKS = "files_private_links";
public static final String CAPABILITIES_FILES_UNDELETE = "files_undelete";
public static final String CAPABILITIES_FILES_VERSIONING = "files_versioning";
public static final String CAPABILITIES_TUS_SUPPORT_PREFIX = "tus_support_";
public static final String CAPABILITIES_TUS_SUPPORT_VERSION = CAPABILITIES_TUS_SUPPORT_PREFIX + "version";
public static final String CAPABILITIES_TUS_SUPPORT_RESUMABLE = CAPABILITIES_TUS_SUPPORT_PREFIX + "resumable";
public static final String CAPABILITIES_TUS_SUPPORT_EXTENSION = CAPABILITIES_TUS_SUPPORT_PREFIX + "extension";
public static final String CAPABILITIES_TUS_SUPPORT_MAX_CHUNK_SIZE = CAPABILITIES_TUS_SUPPORT_PREFIX + "maxChunkSize";
public static final String CAPABILITIES_TUS_SUPPORT_HTTP_METHOD_OVERRIDE = CAPABILITIES_TUS_SUPPORT_PREFIX + "httpMethodOverride";
public static final String CAPABILITIES_SHARING_API_ENABLED = "sharing_api_enabled";
public static final String CAPABILITIES_SHARING_FEDERATION_INCOMING = "sharing_federation_incoming";
public static final String CAPABILITIES_SHARING_FEDERATION_OUTGOING = "sharing_federation_outgoing";
public static final String CAPABILITIES_SHARING_PUBLIC_ENABLED = "sharing_public_enabled";
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS = "sharing_public_expire_date_days";
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED = "sharing_public_expire_date_enabled";
public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED = "sharing_public_expire_date_enforced";
public static final String CAPABILITIES_SHARING_PUBLIC_MULTIPLE = "sharing_public_multiple";
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED = "sharing_public_password_enforced";
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY = "sharing_public_password_enforced_read_only";
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE = "sharing_public_password_enforced_read_write";
public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY = "sharing_public_password_enforced_public_only";
public static final String CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY = "supports_upload_only";
public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload";
public static final String CAPABILITIES_SHARING_RESHARING = "sharing_resharing";
public static final String CAPABILITIES_SHARING_USER_PROFILE_PICTURE = "sharing_user_profile_picture";
public static final String CAPABILITIES_SPACES_PREFIX = "spaces_";
public static final String CAPABILITIES_PASSWORD_POLICY_PREFIX = "password_policy_";
public static final String CAPABILITIES_VERSION_EDITION = "version_edition";
public static final String CAPABILITIES_VERSION_MAJOR = "version_major";
public static final String CAPABILITIES_VERSION_MICRO = "version_micro";
public static final String CAPABILITIES_VERSION_MINOR = "version_minor";
public static final String CAPABILITIES_VERSION_STRING = "version_string";
public static final String LEGACY_CAPABILITIES_VERSION_MAYOR = "version_mayor";
// Columns of filelist table (legacy)
public static final String FILE_ACCOUNT_OWNER = "file_owner";
public static final String FILE_CONTENT_LENGTH = "content_length";
public static final String FILE_CONTENT_TYPE = "content_type";
public static final String FILE_CREATION = "created";
public static final String FILE_ETAG = "etag";
public static final String FILE_ETAG_IN_CONFLICT = "etag_in_conflict";
public static final String FILE_REMOTE_ETAG = "remoteEtag";
public static final String FILE_IS_DOWNLOADING = "is_downloading";
public static final String FILE_KEEP_IN_SYNC = "keep_in_sync";
public static final String FILE_LAST_SYNC_DATE_FOR_DATA = "last_sync_date_for_data";
public static final String FILE_MODIFIED = "modified";
public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data";
public static final String FILE_NAME = "filename";
public static final String FILE_PARENT = "parent";
public static final String FILE_PATH = "path";
public static final String FILE_PERMISSIONS = "permissions";
public static final String FILE_PRIVATE_LINK = "private_link";
public static final String FILE_REMOTE_ID = "remote_id";
public static final String FILE_SHARED_VIA_LINK = "share_by_link";
public static final String FILE_SHARED_WITH_SHAREE = "shared_via_users";
public static final String FILE_STORAGE_PATH = "media_path";
public static final String FILE_TREE_ETAG = "tree_etag";
public static final String FILE_UPDATE_THUMBNAIL = "update_thumbnail";
// Columns of list_of_uploads table
public static final String UPLOAD_ACCOUNT_NAME = "account_name";
public static final String UPLOAD_CREATED_BY = "created_by";
public static final String UPLOAD_FILE_SIZE = "file_size";
public static final String UPLOAD_FORCE_OVERWRITE = "force_overwrite";
public static final String UPLOAD_LAST_RESULT = "last_result";
public static final String UPLOAD_LOCAL_BEHAVIOUR = "local_behaviour";
public static final String UPLOAD_LOCAL_PATH = "local_path";
public static final String UPLOAD_REMOTE_PATH = "remote_path";
public static final String UPLOAD_STATUS = "status";
public static final String UPLOAD_TRANSFER_ID = "transfer_id";
public static final String UPLOAD_UPLOAD_END_TIMESTAMP = "upload_end_timestamp";
// Columns of files table
public static final String FILE_OWNER = "owner";
public static final String FILE_SPACE_ID = "spaceId";
}
}
@@ -0,0 +1,145 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import eu.qsfera.android.data.appregistry.db.AppRegistryDao
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity
import eu.qsfera.android.data.capabilities.db.OCCapabilityDao
import eu.qsfera.android.data.capabilities.db.OCCapabilityEntity
import eu.qsfera.android.data.files.db.FileDao
import eu.qsfera.android.data.files.db.OCFileEntity
import eu.qsfera.android.data.files.db.OCFileSyncEntity
import eu.qsfera.android.data.folderbackup.db.FolderBackUpEntity
import eu.qsfera.android.data.folderbackup.db.FolderBackupDao
import eu.qsfera.android.data.migrations.AutoMigration39To40
import eu.qsfera.android.data.migrations.MIGRATION_27_28
import eu.qsfera.android.data.migrations.MIGRATION_28_29
import eu.qsfera.android.data.migrations.MIGRATION_29_30
import eu.qsfera.android.data.migrations.MIGRATION_30_31
import eu.qsfera.android.data.migrations.MIGRATION_31_32
import eu.qsfera.android.data.migrations.MIGRATION_32_33
import eu.qsfera.android.data.migrations.MIGRATION_33_34
import eu.qsfera.android.data.migrations.MIGRATION_34_35
import eu.qsfera.android.data.migrations.MIGRATION_35_36
import eu.qsfera.android.data.migrations.MIGRATION_37_38
import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.data.migrations.MIGRATION_47_48
import eu.qsfera.android.data.migrations.MIGRATION_48_49
import eu.qsfera.android.data.sharing.shares.db.OCShareDao
import eu.qsfera.android.data.sharing.shares.db.OCShareEntity
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity
import eu.qsfera.android.data.spaces.db.SpacesDao
import eu.qsfera.android.data.spaces.db.SpacesEntity
import eu.qsfera.android.data.transfers.db.OCTransferEntity
import eu.qsfera.android.data.transfers.db.TransferDao
import eu.qsfera.android.data.user.db.UserDao
import eu.qsfera.android.data.user.db.UserQuotaEntity
@Database(
entities = [
AppRegistryEntity::class,
FolderBackUpEntity::class,
OCCapabilityEntity::class,
OCFileEntity::class,
OCFileSyncEntity::class,
OCShareEntity::class,
OCTransferEntity::class,
SpacesEntity::class,
SpaceSpecialEntity::class,
UserQuotaEntity::class,
],
autoMigrations = [
AutoMigration(from = 36, to = 37),
AutoMigration(from = 38, to = 39),
AutoMigration(from = 39, to = 40, spec = AutoMigration39To40::class),
AutoMigration(from = 40, to = 41),
AutoMigration(from = 43, to = 44),
AutoMigration(from = 44, to = 45),
AutoMigration(from = 45, to = 46),
AutoMigration(from = 46, to = 47),
],
version = ProviderMeta.DB_VERSION,
exportSchema = true
)
abstract class QSferaDatabase : RoomDatabase() {
abstract fun appRegistryDao(): AppRegistryDao
abstract fun capabilityDao(): OCCapabilityDao
abstract fun fileDao(): FileDao
abstract fun folderBackUpDao(): FolderBackupDao
abstract fun shareDao(): OCShareDao
abstract fun spacesDao(): SpacesDao
abstract fun transferDao(): TransferDao
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: QSferaDatabase? = null
fun getDatabase(
context: Context
): QSferaDatabase =
// if the INSTANCE is not null, then return it,
// if it is, then create the database
INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
QSferaDatabase::class.java,
ProviderMeta.NEW_DB_NAME
)
.addMigrations(
MIGRATION_27_28,
MIGRATION_28_29,
MIGRATION_29_30,
MIGRATION_30_31,
MIGRATION_31_32,
MIGRATION_32_33,
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_37_38,
MIGRATION_41_42,
MIGRATION_42_43,
MIGRATION_47_48,
MIGRATION_48_49)
.build()
INSTANCE = instance
instance
}
@VisibleForTesting
fun switchToInMemory(context: Context, vararg migrations: Migration) {
INSTANCE = Room.inMemoryDatabaseBuilder(
context.applicationContext,
QSferaDatabase::class.java
).addMigrations(*migrations)
.build()
}
}
}
@@ -0,0 +1,148 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data
import eu.qsfera.android.domain.exceptions.AccountException
import eu.qsfera.android.domain.exceptions.AccountNotFoundException
import eu.qsfera.android.domain.exceptions.AccountNotNewException
import eu.qsfera.android.domain.exceptions.AccountNotTheSameException
import eu.qsfera.android.domain.exceptions.BadOcVersionException
import eu.qsfera.android.domain.exceptions.CancelledException
import eu.qsfera.android.domain.exceptions.ConflictException
import eu.qsfera.android.domain.exceptions.CopyIntoDescendantException
import eu.qsfera.android.domain.exceptions.DelayedForWifiException
import eu.qsfera.android.domain.exceptions.FileNotFoundException
import eu.qsfera.android.domain.exceptions.ForbiddenException
import eu.qsfera.android.domain.exceptions.IncorrectAddressException
import eu.qsfera.android.domain.exceptions.InstanceNotConfiguredException
import eu.qsfera.android.domain.exceptions.InvalidCharacterException
import eu.qsfera.android.domain.exceptions.InvalidCharacterInNameException
import eu.qsfera.android.domain.exceptions.InvalidLocalFileNameException
import eu.qsfera.android.domain.exceptions.InvalidOverwriteException
import eu.qsfera.android.domain.exceptions.LocalFileNotFoundException
import eu.qsfera.android.domain.exceptions.LocalStorageFullException
import eu.qsfera.android.domain.exceptions.LocalStorageNotCopiedException
import eu.qsfera.android.domain.exceptions.LocalStorageNotMovedException
import eu.qsfera.android.domain.exceptions.LocalStorageNotRemovedException
import eu.qsfera.android.domain.exceptions.MoveIntoDescendantException
import eu.qsfera.android.domain.exceptions.NetworkErrorException
import eu.qsfera.android.domain.exceptions.NoConnectionWithServerException
import eu.qsfera.android.domain.exceptions.NoNetworkConnectionException
import eu.qsfera.android.domain.exceptions.OAuth2ErrorAccessDeniedException
import eu.qsfera.android.domain.exceptions.OAuth2ErrorException
import eu.qsfera.android.domain.exceptions.PartialCopyDoneException
import eu.qsfera.android.domain.exceptions.PartialMoveDoneException
import eu.qsfera.android.domain.exceptions.QuotaExceededException
import eu.qsfera.android.domain.exceptions.RedirectToNonSecureException
import eu.qsfera.android.domain.exceptions.ResourceLockedException
import eu.qsfera.android.domain.exceptions.SSLErrorException
import eu.qsfera.android.domain.exceptions.ServerConnectionTimeoutException
import eu.qsfera.android.domain.exceptions.ServerNotReachableException
import eu.qsfera.android.domain.exceptions.ServerResponseTimeoutException
import eu.qsfera.android.domain.exceptions.ServiceUnavailableException
import eu.qsfera.android.domain.exceptions.ShareForbiddenException
import eu.qsfera.android.domain.exceptions.ShareNotFoundException
import eu.qsfera.android.domain.exceptions.ShareWrongParameterException
import eu.qsfera.android.domain.exceptions.SpecificForbiddenException
import eu.qsfera.android.domain.exceptions.SpecificMethodNotAllowedException
import eu.qsfera.android.domain.exceptions.SpecificServiceUnavailableException
import eu.qsfera.android.domain.exceptions.SpecificUnsupportedMediaTypeException
import eu.qsfera.android.domain.exceptions.SyncConflictException
import eu.qsfera.android.domain.exceptions.TooEarlyException
import eu.qsfera.android.domain.exceptions.UnauthorizedException
import eu.qsfera.android.domain.exceptions.UnhandledHttpCodeException
import eu.qsfera.android.domain.exceptions.UnknownErrorException
import eu.qsfera.android.domain.exceptions.WrongServerResponseException
import eu.qsfera.android.lib.common.network.CertificateCombinedException
import eu.qsfera.android.lib.common.operations.RemoteOperationResult
import java.net.SocketTimeoutException
fun <T> executeRemoteOperation(operation: () -> RemoteOperationResult<T>): T {
operation.invoke().also {
return handleRemoteOperationResult(it)
}
}
private fun <T> handleRemoteOperationResult(
remoteOperationResult: RemoteOperationResult<T>
): T {
if (remoteOperationResult.isSuccess) {
return remoteOperationResult.data
}
when (remoteOperationResult.code) {
RemoteOperationResult.ResultCode.WRONG_CONNECTION -> throw NoConnectionWithServerException()
RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION -> throw NoNetworkConnectionException()
RemoteOperationResult.ResultCode.TIMEOUT ->
if (remoteOperationResult.exception is SocketTimeoutException) throw ServerResponseTimeoutException()
else throw ServerConnectionTimeoutException()
RemoteOperationResult.ResultCode.HOST_NOT_AVAILABLE -> throw ServerNotReachableException()
RemoteOperationResult.ResultCode.SERVICE_UNAVAILABLE -> throw ServiceUnavailableException()
RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED -> throw remoteOperationResult.exception as CertificateCombinedException
RemoteOperationResult.ResultCode.BAD_OC_VERSION -> throw BadOcVersionException()
RemoteOperationResult.ResultCode.INCORRECT_ADDRESS -> throw IncorrectAddressException()
RemoteOperationResult.ResultCode.SSL_ERROR -> throw SSLErrorException()
RemoteOperationResult.ResultCode.UNAUTHORIZED -> throw UnauthorizedException()
RemoteOperationResult.ResultCode.INSTANCE_NOT_CONFIGURED -> throw InstanceNotConfiguredException()
RemoteOperationResult.ResultCode.FILE_NOT_FOUND -> throw FileNotFoundException()
RemoteOperationResult.ResultCode.OAUTH2_ERROR -> throw OAuth2ErrorException()
RemoteOperationResult.ResultCode.OAUTH2_ERROR_ACCESS_DENIED -> throw OAuth2ErrorAccessDeniedException()
RemoteOperationResult.ResultCode.ACCOUNT_NOT_NEW -> throw AccountNotNewException()
RemoteOperationResult.ResultCode.ACCOUNT_NOT_THE_SAME -> throw AccountNotTheSameException()
RemoteOperationResult.ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION -> throw RedirectToNonSecureException()
RemoteOperationResult.ResultCode.UNHANDLED_HTTP_CODE -> throw UnhandledHttpCodeException()
RemoteOperationResult.ResultCode.UNKNOWN_ERROR -> throw UnknownErrorException()
RemoteOperationResult.ResultCode.CANCELLED -> throw CancelledException()
RemoteOperationResult.ResultCode.INVALID_LOCAL_FILE_NAME -> throw InvalidLocalFileNameException()
RemoteOperationResult.ResultCode.INVALID_OVERWRITE -> throw InvalidOverwriteException()
RemoteOperationResult.ResultCode.CONFLICT -> throw ConflictException()
RemoteOperationResult.ResultCode.SYNC_CONFLICT -> throw SyncConflictException()
RemoteOperationResult.ResultCode.LOCAL_STORAGE_FULL -> throw LocalStorageFullException()
RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED -> throw LocalStorageNotMovedException()
RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_COPIED -> throw LocalStorageNotCopiedException()
RemoteOperationResult.ResultCode.QUOTA_EXCEEDED -> throw QuotaExceededException()
RemoteOperationResult.ResultCode.ACCOUNT_NOT_FOUND -> throw AccountNotFoundException()
RemoteOperationResult.ResultCode.ACCOUNT_EXCEPTION -> throw AccountException()
RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME -> throw InvalidCharacterInNameException()
RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_REMOVED -> throw LocalStorageNotRemovedException()
RemoteOperationResult.ResultCode.FORBIDDEN -> throw ForbiddenException()
RemoteOperationResult.ResultCode.SPECIFIC_FORBIDDEN -> throw SpecificForbiddenException()
RemoteOperationResult.ResultCode.INVALID_MOVE_INTO_DESCENDANT -> throw MoveIntoDescendantException()
RemoteOperationResult.ResultCode.INVALID_COPY_INTO_DESCENDANT -> throw CopyIntoDescendantException()
RemoteOperationResult.ResultCode.PARTIAL_MOVE_DONE -> throw PartialMoveDoneException()
RemoteOperationResult.ResultCode.PARTIAL_COPY_DONE -> throw PartialCopyDoneException()
RemoteOperationResult.ResultCode.SHARE_WRONG_PARAMETER -> throw ShareWrongParameterException()
RemoteOperationResult.ResultCode.WRONG_SERVER_RESPONSE -> throw WrongServerResponseException()
RemoteOperationResult.ResultCode.INVALID_CHARACTER_DETECT_IN_SERVER -> throw InvalidCharacterException()
RemoteOperationResult.ResultCode.DELAYED_FOR_WIFI -> throw DelayedForWifiException()
RemoteOperationResult.ResultCode.LOCAL_FILE_NOT_FOUND -> throw LocalFileNotFoundException()
RemoteOperationResult.ResultCode.SPECIFIC_SERVICE_UNAVAILABLE -> throw SpecificServiceUnavailableException(remoteOperationResult.httpPhrase)
RemoteOperationResult.ResultCode.SPECIFIC_UNSUPPORTED_MEDIA_TYPE -> throw SpecificUnsupportedMediaTypeException()
RemoteOperationResult.ResultCode.SPECIFIC_METHOD_NOT_ALLOWED -> throw SpecificMethodNotAllowedException(remoteOperationResult.httpPhrase)
RemoteOperationResult.ResultCode.SHARE_NOT_FOUND -> throw ShareNotFoundException(remoteOperationResult.httpPhrase)
RemoteOperationResult.ResultCode.SHARE_FORBIDDEN -> throw ShareForbiddenException(remoteOperationResult.httpPhrase)
RemoteOperationResult.ResultCode.TOO_EARLY -> throw TooEarlyException()
RemoteOperationResult.ResultCode.NETWORK_ERROR -> throw NetworkErrorException()
RemoteOperationResult.ResultCode.RESOURCE_LOCKED -> throw ResourceLockedException()
else -> throw Exception("An unknown error has occurred")
}
}
@@ -0,0 +1,45 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.appregistry.datasources
import eu.qsfera.android.domain.appregistry.model.AppRegistry
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
import kotlinx.coroutines.flow.Flow
interface LocalAppRegistryDataSource {
fun getAppRegistryForMimeTypeAsStream(
accountName: String,
mimeType: String,
): Flow<AppRegistryMimeType?>
fun getAppRegistryWhichAllowCreation(
accountName: String,
): Flow<List<AppRegistryMimeType>>
fun saveAppRegistryForAccount(
appRegistry: AppRegistry
)
fun deleteAppRegistryForAccount(
accountName: String,
)
}
@@ -0,0 +1,45 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.appregistry.datasources
import eu.qsfera.android.domain.appregistry.model.AppRegistry
interface RemoteAppRegistryDataSource {
fun getAppRegistryForAccount(
accountName: String,
appUrl: String?,
): AppRegistry
fun getUrlToOpenInWeb(
accountName: String,
openWebEndpoint: String,
fileId: String,
appName: String,
): String
fun createFileWithAppProvider(
accountName: String,
createFileWithAppProviderEndpoint: String,
parentContainerId: String,
filename: String,
): String
}
@@ -0,0 +1,109 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.appregistry.datasources.implementation
import eu.qsfera.android.data.appregistry.datasources.LocalAppRegistryDataSource
import eu.qsfera.android.data.appregistry.db.AppRegistryDao
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity
import eu.qsfera.android.domain.appregistry.model.AppRegistry
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
import eu.qsfera.android.domain.appregistry.model.AppRegistryProvider
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.lang.reflect.Type
class OCLocalAppRegistryDataSource(
private val appRegistryDao: AppRegistryDao,
) : LocalAppRegistryDataSource {
override fun getAppRegistryForMimeTypeAsStream(accountName: String, mimeType: String): Flow<AppRegistryMimeType?> =
appRegistryDao.getAppRegistryForMimeType(accountName, mimeType).map { it?.toModel() }
override fun getAppRegistryWhichAllowCreation(accountName: String): Flow<List<AppRegistryMimeType>> =
appRegistryDao.getAppRegistryWhichAllowCreation(accountName).map { listAppRegistryEntities ->
listAppRegistryEntities.map { it.toModel() }
}
override fun saveAppRegistryForAccount(appRegistry: AppRegistry) {
val appRegistryEntitiesToInsert = mutableListOf<AppRegistryEntity>()
val newAppRegistryEntities = appRegistry.toEntities(appRegistry.accountName)
newAppRegistryEntities.forEach { appRegistryEntity ->
appRegistryEntitiesToInsert.add(appRegistryEntity)
}
appRegistryDao.deleteAppRegistryForAccount(appRegistry.accountName)
appRegistryDao.upsertAppRegistries(appRegistryEntitiesToInsert)
}
override fun deleteAppRegistryForAccount(accountName: String) {
appRegistryDao.deleteAppRegistryForAccount(accountName)
}
private fun AppRegistry.toEntities(accountName: String): List<AppRegistryEntity> =
mimetypes.map { appRegistryMimeTypes ->
AppRegistryEntity(
accountName = accountName,
mimeType = appRegistryMimeTypes.mimeType,
ext = appRegistryMimeTypes.ext,
appProviders = appRegistryMimeTypes.appProviders.toJsonString(),
name = appRegistryMimeTypes.name,
icon = appRegistryMimeTypes.icon,
description = appRegistryMimeTypes.description,
allowCreation = appRegistryMimeTypes.allowCreation,
defaultApplication = appRegistryMimeTypes.defaultApplication,
)
}
private fun AppRegistryEntity.toModel(): AppRegistryMimeType =
AppRegistryMimeType(
mimeType = mimeType,
ext = ext,
appProviders = appProviders.toAppRegistryProvider(),
name = name,
icon = icon,
description = description,
allowCreation = allowCreation,
defaultApplication = defaultApplication,
)
private fun List<AppRegistryProvider>.toJsonString(): String {
val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val type: Type = Types.newParameterizedType(List::class.java, AppRegistryProvider::class.java)
val jsonAdapter: JsonAdapter<List<AppRegistryProvider>> = moshi.adapter(type)
return jsonAdapter.toJson(this)
}
private fun String.toAppRegistryProvider(): List<AppRegistryProvider> {
val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val type: Type = Types.newParameterizedType(List::class.java, AppRegistryProvider::class.java)
val jsonAdapter: JsonAdapter<List<AppRegistryProvider>> = moshi.adapter(type)
return jsonAdapter.fromJson(this) ?: emptyList()
}
}
@@ -0,0 +1,90 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.appregistry.datasources.implementation
import eu.qsfera.android.data.ClientManager
import eu.qsfera.android.data.appregistry.datasources.RemoteAppRegistryDataSource
import eu.qsfera.android.data.executeRemoteOperation
import eu.qsfera.android.domain.appregistry.model.AppRegistry
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
import eu.qsfera.android.domain.appregistry.model.AppRegistryProvider
import eu.qsfera.android.lib.resources.appregistry.responses.AppRegistryResponse
class OCRemoteAppRegistryDataSource(
private val clientManager: ClientManager
) : RemoteAppRegistryDataSource {
override fun getAppRegistryForAccount(accountName: String, appUrl: String?): AppRegistry =
executeRemoteOperation {
clientManager.getAppRegistryService(accountName).getAppRegistry(appUrl)
}.toModel(accountName)
override fun getUrlToOpenInWeb(
accountName: String,
openWebEndpoint: String,
fileId: String,
appName: String,
): String =
executeRemoteOperation {
clientManager.getAppRegistryService(accountName).getUrlToOpenInWeb(
openWebEndpoint = openWebEndpoint,
fileId = fileId,
appName = appName,
)
}
override fun createFileWithAppProvider(
accountName: String,
createFileWithAppProviderEndpoint: String,
parentContainerId: String,
filename: String,
): String =
executeRemoteOperation {
clientManager.getAppRegistryService(accountName).createFileWithAppProvider(
createFileWithAppProviderEndpoint = createFileWithAppProviderEndpoint,
parentContainerId = parentContainerId,
filename = filename,
)
}
private fun AppRegistryResponse.toModel(accountName: String) =
AppRegistry(
accountName = accountName,
mimetypes = value.map { appRegistryMimeTypeResponse ->
AppRegistryMimeType(
mimeType = appRegistryMimeTypeResponse.mimeType,
ext = appRegistryMimeTypeResponse.ext,
appProviders = appRegistryMimeTypeResponse.appProviders.map { appRegistryProviderResponse ->
AppRegistryProvider(
name = appRegistryProviderResponse.name,
productName = appRegistryProviderResponse.productName,
icon = appRegistryProviderResponse.icon
)
},
name = appRegistryMimeTypeResponse.name,
icon = appRegistryMimeTypeResponse.icon,
description = appRegistryMimeTypeResponse.description,
allowCreation = appRegistryMimeTypeResponse.allowCreation,
defaultApplication = appRegistryMimeTypeResponse.defaultApplication,
)
}
)
}
@@ -0,0 +1,71 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.appregistry.db
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import eu.qsfera.android.data.ProviderMeta
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_ACCOUNT_NAME
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_ALLOW_CREATION
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_MIME_TYPE
import kotlinx.coroutines.flow.Flow
@Dao
interface AppRegistryDao {
@Upsert
fun upsertAppRegistries(appRegistryEntities: List<AppRegistryEntity>)
@Query(SELECT_APP_REGISTRY_FOR_ACCOUNT_AND_MIME_TYPE)
fun getAppRegistryForMimeType(
accountName: String,
mimeType: String,
): Flow<AppRegistryEntity?>
@Query(SELECT_APP_REGISTRY_ALLOW_CREATION_FOR_ACCOUNT)
fun getAppRegistryWhichAllowCreation(
accountName: String,
): Flow<List<AppRegistryEntity>>
@Query(DELETE_APP_REGISTRY_FOR_ACCOUNT)
fun deleteAppRegistryForAccount(accountName: String)
companion object {
private const val SELECT_APP_REGISTRY_FOR_ACCOUNT_AND_MIME_TYPE = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME}
WHERE $APP_REGISTRY_ACCOUNT_NAME = :accountName AND $APP_REGISTRY_MIME_TYPE = :mimeType
"""
private const val SELECT_APP_REGISTRY_ALLOW_CREATION_FOR_ACCOUNT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME}
WHERE $APP_REGISTRY_ACCOUNT_NAME = :accountName AND $APP_REGISTRY_ALLOW_CREATION = 1
"""
private const val DELETE_APP_REGISTRY_FOR_ACCOUNT = """
DELETE
FROM ${ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME}
WHERE $APP_REGISTRY_ACCOUNT_NAME = :accountName
"""
}
}
@@ -0,0 +1,56 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.appregistry.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import eu.qsfera.android.data.ProviderMeta
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_ACCOUNT_NAME
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_MIME_TYPE
@Entity(
tableName = ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME,
primaryKeys = [APP_REGISTRY_ACCOUNT_NAME, APP_REGISTRY_MIME_TYPE]
)
data class AppRegistryEntity(
@ColumnInfo(name = APP_REGISTRY_ACCOUNT_NAME)
val accountName: String,
@ColumnInfo(name = APP_REGISTRY_MIME_TYPE)
val mimeType: String,
val ext: String? = null,
@ColumnInfo(name = APP_REGISTRY_APP_PROVIDERS)
val appProviders: String,
val name: String? = null,
val icon: String? = null,
val description: String? = null,
@ColumnInfo(name = APP_REGISTRY_ALLOW_CREATION)
val allowCreation: Boolean? = null,
@ColumnInfo(name = APP_REGISTRY_DEFAULT_APPLICATION)
val defaultApplication: String? = null
) {
companion object {
const val APP_REGISTRY_MIME_TYPES = "mime_types"
const val APP_REGISTRY_ACCOUNT_NAME = "account_name"
const val APP_REGISTRY_MIME_TYPE = "mime_type"
const val APP_REGISTRY_APP_PROVIDERS = "app_providers"
const val APP_REGISTRY_ALLOW_CREATION = "allow_creation"
const val APP_REGISTRY_DEFAULT_APPLICATION = "default_application"
}
}
@@ -0,0 +1,70 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.appregistry.repository
import eu.qsfera.android.data.appregistry.datasources.LocalAppRegistryDataSource
import eu.qsfera.android.data.appregistry.datasources.RemoteAppRegistryDataSource
import eu.qsfera.android.data.capabilities.datasources.LocalCapabilitiesDataSource
import eu.qsfera.android.domain.appregistry.AppRegistryRepository
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
import kotlinx.coroutines.flow.Flow
class OCAppRegistryRepository(
private val localAppRegistryDataSource: LocalAppRegistryDataSource,
private val remoteAppRegistryDataSource: RemoteAppRegistryDataSource,
private val localCapabilitiesDataSource: LocalCapabilitiesDataSource,
) : AppRegistryRepository {
override fun refreshAppRegistryForAccount(accountName: String) {
val capabilities = localCapabilitiesDataSource.getCapabilitiesForAccount(accountName)
val appUrl = capabilities?.filesAppProviders?.appsUrl?.substring(1)
remoteAppRegistryDataSource.getAppRegistryForAccount(accountName, appUrl).also {
localAppRegistryDataSource.saveAppRegistryForAccount(it)
}
}
override fun getAppRegistryForMimeTypeAsStream(accountName: String, mimeType: String): Flow<AppRegistryMimeType?> =
localAppRegistryDataSource.getAppRegistryForMimeTypeAsStream(accountName, mimeType)
override fun getAppRegistryWhichAllowCreation(accountName: String): Flow<List<AppRegistryMimeType>> =
localAppRegistryDataSource.getAppRegistryWhichAllowCreation(accountName)
override fun getUrlToOpenInWeb(accountName: String, openWebEndpoint: String, fileId: String, appName: String): String =
remoteAppRegistryDataSource.getUrlToOpenInWeb(
accountName = accountName,
openWebEndpoint = openWebEndpoint,
fileId = fileId,
appName = appName,
)
override fun createFileWithAppProvider(
accountName: String,
createFileWithAppProviderEndpoint: String,
parentContainerId: String,
filename: String,
): String =
remoteAppRegistryDataSource.createFileWithAppProvider(
accountName = accountName,
createFileWithAppProviderEndpoint = createFileWithAppProviderEndpoint,
parentContainerId = parentContainerId,
filename = filename,
)
}
@@ -0,0 +1,74 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.authentication
const val SELECTED_ACCOUNT = "select_oc_account"
/**
* OAuth2 user id
*/
const val KEY_USER_ID = "user_id"
/**
* OAuth2 refresh token
*/
const val KEY_OAUTH2_REFRESH_TOKEN = "oc_oauth2_refresh_token"
/**
* OAuth2 scope
*/
const val KEY_OAUTH2_SCOPE = "oc_oauth2_scope"
/**
* Features allowed for the account
*/
const val KEY_FEATURE_ALLOWED = "KEY_FEATURE_ALLOWED"
const val KEY_FEATURE_SPACES = "KEY_FEATURE_SPACES"
/**
* OIDC Client Registration
*/
/**
* Preferred username from OIDC id_token, used for login_hint
*/
const val KEY_PREFERRED_USERNAME = "oc_preferred_username"
/**
* OIDC issuer URL from webfinger, used for token refresh OIDC discovery
*/
const val KEY_OIDC_ISSUER = "oc_oidc_issuer"
const val KEY_CLIENT_REGISTRATION_CLIENT_ID = "client_id"
const val KEY_CLIENT_REGISTRATION_CLIENT_SECRET = "client_secret"
const val KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE = "client_secret_expires_at"
/** Query parameters to retrieve the authorization code. More info: https://tools.ietf.org/html/rfc6749#section-4.1.1 */
const val QUERY_PARAMETER_REDIRECT_URI = "redirect_uri"
const val QUERY_PARAMETER_CLIENT_ID = "client_id"
const val QUERY_PARAMETER_RESPONSE_TYPE = "response_type"
const val QUERY_PARAMETER_SCOPE = "scope"
const val QUERY_PARAMETER_PROMPT = "prompt"
const val QUERY_PARAMETER_CODE_CHALLENGE = "code_challenge"
const val QUERY_PARAMETER_CODE_CHALLENGE_METHOD = "code_challenge_method"
const val QUERY_PARAMETER_STATE = "state"
const val QUERY_PARAMETER_USER = "user"
const val QUERY_PARAMETER_LOGIN_HINT = "login_hint"
@@ -0,0 +1,52 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.authentication.datasources
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
import eu.qsfera.android.domain.server.model.ServerInfo
import eu.qsfera.android.domain.user.model.UserInfo
interface LocalAuthenticationDataSource {
fun addBasicAccount(
userName: String,
lastPermanentLocation: String?,
password: String,
serverInfo: ServerInfo,
userInfo: UserInfo,
updateAccountWithUsername: String?
): String
fun addOAuthAccount(
userName: String,
lastPermanentLocation: String?,
authTokenType: String,
accessToken: String,
serverInfo: ServerInfo,
userInfo: UserInfo,
refreshToken: String,
scope: String?,
updateAccountWithUsername: String?,
clientRegistrationInfo: ClientRegistrationInfo?
): String
fun supportsOAuth2(accountName: String): Boolean
fun getBaseUrl(accountName: String): String
}
@@ -0,0 +1,27 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.authentication.datasources
import eu.qsfera.android.domain.user.model.UserInfo
interface RemoteAuthenticationDataSource {
// Returns a Pair with UserInfo and last permanent redirection
fun loginBasic(serverPath: String, username: String, password: String): Pair<UserInfo, String?>
fun loginOAuth(serverPath: String, username: String, accessToken: String): Pair<UserInfo, String?>
}
@@ -0,0 +1,258 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.authentication.datasources.implementation
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.net.Uri
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_ID
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_SECRET
import eu.qsfera.android.data.authentication.KEY_FEATURE_ALLOWED
import eu.qsfera.android.data.authentication.KEY_FEATURE_SPACES
import eu.qsfera.android.data.authentication.KEY_OAUTH2_REFRESH_TOKEN
import eu.qsfera.android.data.authentication.KEY_OAUTH2_SCOPE
import eu.qsfera.android.data.authentication.SELECTED_ACCOUNT
import eu.qsfera.android.data.authentication.datasources.LocalAuthenticationDataSource
import eu.qsfera.android.data.providers.SharedPreferencesProvider
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
import eu.qsfera.android.domain.exceptions.AccountNotFoundException
import eu.qsfera.android.domain.exceptions.AccountNotNewException
import eu.qsfera.android.domain.exceptions.AccountNotTheSameException
import eu.qsfera.android.domain.server.model.ServerInfo
import eu.qsfera.android.domain.user.model.UserInfo
import eu.qsfera.android.lib.common.SingleSessionManager
import eu.qsfera.android.lib.common.accounts.AccountUtils
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.ACCOUNT_VERSION
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_DISPLAY_NAME
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_ID
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_SUPPORTS_OAUTH2
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.OAUTH_SUPPORTED_TRUE
import eu.qsfera.android.lib.common.authentication.QSferaBasicCredentials
import eu.qsfera.android.lib.common.authentication.QSferaBearerCredentials
import timber.log.Timber
import java.util.Locale
class OCLocalAuthenticationDataSource(
private val context: Context,
private val accountManager: AccountManager,
private val preferencesProvider: SharedPreferencesProvider,
private val accountType: String
) : LocalAuthenticationDataSource {
override fun addBasicAccount(
userName: String,
lastPermanentLocation: String?,
password: String,
serverInfo: ServerInfo,
userInfo: UserInfo,
updateAccountWithUsername: String?
): String =
addAccount(
lastPermanentLocation = lastPermanentLocation,
serverInfo = serverInfo,
userName = userName,
password = password,
updateAccountWithUsername = updateAccountWithUsername
).also { account ->
updateAccountWithUsername?.let {
accountManager.setPassword(account, password)
SingleSessionManager.getDefaultSingleton().refreshCredentialsForAccount(
account.name, QSferaBasicCredentials(userName, password)
)
}
updateUserAndServerInfo(account, serverInfo, userInfo)
}.name
override fun addOAuthAccount(
userName: String,
lastPermanentLocation: String?,
authTokenType: String,
accessToken: String,
serverInfo: ServerInfo,
userInfo: UserInfo,
refreshToken: String,
scope: String?,
updateAccountWithUsername: String?,
clientRegistrationInfo: ClientRegistrationInfo?
): String =
addAccount(
lastPermanentLocation = lastPermanentLocation,
serverInfo = serverInfo,
userName = userName,
updateAccountWithUsername = updateAccountWithUsername
).also {
updateUserAndServerInfo(it, serverInfo, userInfo)
accountManager.setAuthToken(it, authTokenType, accessToken)
updateAccountWithUsername?.let { userName ->
SingleSessionManager.getDefaultSingleton().refreshCredentialsForAccount(
it.name, QSferaBearerCredentials(userName, accessToken)
)
}
clientRegistrationInfo?.let { clientRegistrationInfo ->
accountManager.apply {
setUserData(it, KEY_CLIENT_REGISTRATION_CLIENT_ID, clientRegistrationInfo.clientId)
setUserData(it, KEY_CLIENT_REGISTRATION_CLIENT_SECRET, clientRegistrationInfo.clientSecret)
setUserData(it, KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE, clientRegistrationInfo.clientSecretExpiration.toString())
}
}
accountManager.setUserData(it, KEY_SUPPORTS_OAUTH2, OAUTH_SUPPORTED_TRUE)
accountManager.setUserData(it, KEY_OAUTH2_REFRESH_TOKEN, refreshToken)
scope?.run {
accountManager.setUserData(it, KEY_OAUTH2_SCOPE, this)
}
}.name
/**
* Add new account to account manager, shared preferences and returns account
*/
private fun addAccount(
lastPermanentLocation: String?,
serverInfo: ServerInfo,
userName: String,
password: String = "",
updateAccountWithUsername: String?
): Account {
lastPermanentLocation?.let {
serverInfo.baseUrl = it
}
val uri = Uri.parse(serverInfo.baseUrl)
val accountName = AccountUtils.buildAccountName(uri, userName)
// Check if the entered user matches the user of the account to update
if (!updateAccountWithUsername.isNullOrBlank() && accountName != updateAccountWithUsername) {
throw AccountNotTheSameException()
}
val account = getAccountIfExists(accountName)
if (account != null) {
if (updateAccountWithUsername.isNullOrBlank()) {
Timber.d("The account already exists")
throw AccountNotNewException()
} else {
return account // Account won't be null, since we've already checked it exists
}
} else {
val newAccount = Account(accountName, accountType)
// with external authorizations, the password is never input in the app
accountManager.addAccountExplicitly(newAccount, password, null)
// Only fresh accounts will support spaces
accountManager.setUserData(newAccount, KEY_FEATURE_SPACES, KEY_FEATURE_ALLOWED)
/// add the new account as default in preferences, if there is none already
val defaultAccount: Account? = getCurrentAccount()
if (defaultAccount == null) {
preferencesProvider.putString(SELECTED_ACCOUNT, accountName)
}
return newAccount
}
}
private fun updateUserAndServerInfo(
newAccount: Account,
serverInfo: ServerInfo,
userInfo: UserInfo
) {
// include account version with the new account
accountManager.setUserData(
newAccount,
KEY_OC_ACCOUNT_VERSION,
ACCOUNT_VERSION.toString()
)
accountManager.setUserData(
newAccount, KEY_OC_BASE_URL, serverInfo.baseUrl
)
accountManager.setUserData(
newAccount, KEY_DISPLAY_NAME, userInfo.displayName
)
accountManager.setUserData(
newAccount, KEY_ID, userInfo.id
)
}
private fun getAccountIfExists(accountName: String): Account? {
val ocAccounts: Array<Account> = getAccounts()
var lastAtPos = accountName.lastIndexOf("@")
val hostAndPort = accountName.substring(lastAtPos + 1)
val username = accountName.substring(0, lastAtPos)
var otherHostAndPort: String
var otherUsername: String
val currentLocale: Locale = context.resources.configuration.locale
for (otherAccount in ocAccounts) {
lastAtPos = otherAccount.name.lastIndexOf("@")
otherHostAndPort = otherAccount.name.substring(lastAtPos + 1)
otherUsername = otherAccount.name.substring(0, lastAtPos)
if (otherHostAndPort == hostAndPort && otherUsername.lowercase(currentLocale) == username.lowercase(currentLocale)) {
return otherAccount
}
}
return null
}
private fun getAccounts(): Array<Account> =
accountManager.getAccountsByType(accountType)
private fun getCurrentAccount(): Account? {
val ocAccounts = getAccounts()
var defaultAccount: Account? = null
val accountName = preferencesProvider.getString(SELECTED_ACCOUNT, null)
// account validation: the saved account MUST be in the list of qsfera Accounts known by the AccountManager
if (accountName != null) {
for (account in ocAccounts) {
if (account.name == accountName) {
defaultAccount = account
break
}
}
}
return defaultAccount
}
override fun supportsOAuth2(accountName: String): Boolean {
val account = getAccountIfExists(accountName) ?: throw AccountNotFoundException()
return accountManager.getUserData(account, KEY_SUPPORTS_OAUTH2) == OAUTH_SUPPORTED_TRUE
}
override fun getBaseUrl(accountName: String): String {
val account = getAccountIfExists(accountName) ?: throw AccountNotFoundException()
return accountManager.getUserData(account, KEY_OC_BASE_URL)
}
}
@@ -0,0 +1,65 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.authentication.datasources.implementation
import eu.qsfera.android.data.ClientManager
import eu.qsfera.android.data.authentication.datasources.RemoteAuthenticationDataSource
import eu.qsfera.android.data.executeRemoteOperation
import eu.qsfera.android.data.user.datasources.implementation.toDomain
import eu.qsfera.android.domain.user.model.UserInfo
import eu.qsfera.android.lib.common.QSferaClient
import eu.qsfera.android.lib.common.QSferaClient.WEBDAV_FILES_PATH_4_0
import eu.qsfera.android.lib.common.authentication.QSferaCredentials
import eu.qsfera.android.lib.common.authentication.QSferaCredentialsFactory
import eu.qsfera.android.lib.resources.files.GetBaseUrlRemoteOperation
import eu.qsfera.android.lib.resources.users.GetRemoteUserInfoOperation
class OCRemoteAuthenticationDataSource(
private val clientManager: ClientManager
) : RemoteAuthenticationDataSource {
override fun loginBasic(serverPath: String, username: String, password: String): Pair<UserInfo, String?> =
login(QSferaCredentialsFactory.newBasicCredentials(username, password), serverPath)
override fun loginOAuth(serverPath: String, username: String, accessToken: String): Pair<UserInfo, String?> =
login(QSferaCredentialsFactory.newBearerCredentials(username, accessToken), serverPath)
private fun login(qsferaCredentials: QSferaCredentials, serverPath: String): Pair<UserInfo, String?> {
val client: QSferaClient =
clientManager.getClientForAnonymousCredentials(
path = serverPath,
requiresNewClient = false
).apply { credentials = qsferaCredentials }
val getBaseUrlRemoteOperation = GetBaseUrlRemoteOperation()
val rawBaseUrl = executeRemoteOperation { getBaseUrlRemoteOperation.execute(client) }
val userBaseUri = rawBaseUrl?.replace(WEBDAV_FILES_PATH_4_0, "")
?: client.baseUri.toString()
// Get user info. It is needed to save the account into the account manager
lateinit var userInfo: UserInfo
executeRemoteOperation {
GetRemoteUserInfoOperation().execute(client)
}.let { userInfo = it.toDomain() }
return Pair(userInfo, userBaseUri)
}
}
@@ -0,0 +1,91 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.authentication.repository
import eu.qsfera.android.data.authentication.datasources.LocalAuthenticationDataSource
import eu.qsfera.android.data.authentication.datasources.RemoteAuthenticationDataSource
import eu.qsfera.android.domain.authentication.AuthenticationRepository
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
import eu.qsfera.android.domain.server.model.ServerInfo
import eu.qsfera.android.domain.user.model.UserInfo
class OCAuthenticationRepository(
private val localAuthenticationDataSource: LocalAuthenticationDataSource,
private val remoteAuthenticationDataSource: RemoteAuthenticationDataSource
) : AuthenticationRepository {
override fun loginBasic(
serverInfo: ServerInfo,
username: String,
password: String,
updateAccountWithUsername: String?
): String {
val userInfoAndRedirectionPath: Pair<UserInfo, String?> =
remoteAuthenticationDataSource.loginBasic(
serverPath = serverInfo.baseUrl,
username = username,
password = password
)
return localAuthenticationDataSource.addBasicAccount(
userName = username,
lastPermanentLocation = userInfoAndRedirectionPath.second,
password = password,
serverInfo = serverInfo,
userInfo = userInfoAndRedirectionPath.first,
updateAccountWithUsername = updateAccountWithUsername
)
}
override fun loginOAuth(
serverInfo: ServerInfo,
username: String,
authTokenType: String,
accessToken: String,
refreshToken: String,
scope: String?,
updateAccountWithUsername: String?,
clientRegistrationInfo: ClientRegistrationInfo?
): String {
val userInfoAndRedirectionPath: Pair<UserInfo, String?> =
remoteAuthenticationDataSource.loginOAuth(
serverPath = serverInfo.baseUrl,
username = username,
accessToken = accessToken
)
return localAuthenticationDataSource.addOAuthAccount(
userName = username.ifBlank { userInfoAndRedirectionPath.first.id },
lastPermanentLocation = userInfoAndRedirectionPath.second,
authTokenType = authTokenType,
accessToken = accessToken,
serverInfo = serverInfo,
userInfo = userInfoAndRedirectionPath.first,
refreshToken = refreshToken,
scope = scope,
updateAccountWithUsername = updateAccountWithUsername,
clientRegistrationInfo = clientRegistrationInfo
)
}
override fun supportsOAuth2UseCase(accountName: String): Boolean =
localAuthenticationDataSource.supportsOAuth2(accountName)
override fun getBaseUrl(accountName: String): String = localAuthenticationDataSource.getBaseUrl(accountName)
}
@@ -0,0 +1,38 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.datasources
import androidx.lifecycle.LiveData
import eu.qsfera.android.domain.capabilities.model.OCCapability
interface LocalCapabilitiesDataSource {
fun getCapabilitiesForAccountAsLiveData(
accountName: String
): LiveData<OCCapability?>
fun getCapabilitiesForAccount(
accountName: String
): OCCapability?
fun insertCapabilities(ocCapabilities: List<OCCapability>)
fun deleteCapabilitiesForAccount(accountName: String)
}
@@ -0,0 +1,28 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.datasources
import eu.qsfera.android.domain.capabilities.model.OCCapability
interface RemoteCapabilitiesDataSource {
fun getCapabilities(
accountName: String
): OCCapability
}
@@ -0,0 +1,129 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.datasources.implementation
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import eu.qsfera.android.data.capabilities.datasources.LocalCapabilitiesDataSource
import eu.qsfera.android.data.capabilities.db.OCCapabilityDao
import eu.qsfera.android.data.capabilities.db.OCCapabilityEntity
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
import eu.qsfera.android.domain.capabilities.model.OCCapability
class OCLocalCapabilitiesDataSource(
private val ocCapabilityDao: OCCapabilityDao,
) : LocalCapabilitiesDataSource {
override fun getCapabilitiesForAccountAsLiveData(accountName: String): LiveData<OCCapability?> =
ocCapabilityDao.getCapabilitiesForAccountAsLiveData(accountName).map { ocCapabilityEntity ->
ocCapabilityEntity?.toModel()
}
override fun getCapabilitiesForAccount(accountName: String): OCCapability? =
ocCapabilityDao.getCapabilitiesForAccount(accountName)?.toModel()
override fun insertCapabilities(ocCapabilities: List<OCCapability>) {
ocCapabilityDao.replace(
ocCapabilities.map { ocCapability -> ocCapability.toEntity() }
)
}
override fun deleteCapabilitiesForAccount(accountName: String) {
ocCapabilityDao.deleteByAccountName(accountName)
}
companion object {
@VisibleForTesting
fun OCCapabilityEntity.toModel(): OCCapability =
OCCapability(
id = id,
accountName = accountName,
versionMajor = versionMajor,
versionMinor = versionMinor,
versionMicro = versionMicro,
versionString = versionString,
versionEdition = versionEdition,
corePollInterval = corePollInterval,
davChunkingVersion = davChunkingVersion,
filesSharingApiEnabled = CapabilityBooleanType.fromValue(filesSharingApiEnabled),
filesSharingPublicEnabled = CapabilityBooleanType.fromValue(filesSharingPublicEnabled),
filesSharingPublicPasswordEnforced = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforced),
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforcedReadOnly),
filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforcedReadWrite),
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforcedUploadOnly),
filesSharingPublicExpireDateEnabled = CapabilityBooleanType.fromValue(filesSharingPublicExpireDateEnabled),
filesSharingPublicExpireDateDays = filesSharingPublicExpireDateDays,
filesSharingPublicExpireDateEnforced = CapabilityBooleanType.fromValue(filesSharingPublicExpireDateEnforced),
filesSharingPublicUpload = CapabilityBooleanType.fromValue(filesSharingPublicUpload),
filesSharingPublicMultiple = CapabilityBooleanType.fromValue(filesSharingPublicMultiple),
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.fromValue(filesSharingPublicSupportsUploadOnly),
filesSharingResharing = CapabilityBooleanType.fromValue(filesSharingResharing),
filesSharingFederationOutgoing = CapabilityBooleanType.fromValue(filesSharingFederationOutgoing),
filesSharingFederationIncoming = CapabilityBooleanType.fromValue(filesSharingFederationIncoming),
filesSharingUserProfilePicture = CapabilityBooleanType.fromValue(filesSharingUserProfilePicture),
filesBigFileChunking = CapabilityBooleanType.fromValue(filesBigFileChunking),
filesUndelete = CapabilityBooleanType.fromValue(filesUndelete),
filesVersioning = CapabilityBooleanType.fromValue(filesVersioning),
filesPrivateLinks = CapabilityBooleanType.fromValue(filesPrivateLinks),
filesAppProviders = appProviders,
filesTusSupport = tusSupport,
spaces = spaces,
passwordPolicy = passwordPolicy,
)
@VisibleForTesting
fun OCCapability.toEntity(): OCCapabilityEntity =
OCCapabilityEntity(
accountName = accountName,
versionMajor = versionMajor,
versionMinor = versionMinor,
versionMicro = versionMicro,
versionString = versionString,
versionEdition = versionEdition,
corePollInterval = corePollInterval,
davChunkingVersion = davChunkingVersion,
filesSharingApiEnabled = filesSharingApiEnabled.value,
filesSharingPublicEnabled = filesSharingPublicEnabled.value,
filesSharingPublicPasswordEnforced = filesSharingPublicPasswordEnforced.value,
filesSharingPublicPasswordEnforcedReadOnly = filesSharingPublicPasswordEnforcedReadOnly.value,
filesSharingPublicPasswordEnforcedReadWrite = filesSharingPublicPasswordEnforcedReadWrite.value,
filesSharingPublicPasswordEnforcedUploadOnly = filesSharingPublicPasswordEnforcedUploadOnly.value,
filesSharingPublicExpireDateEnabled = filesSharingPublicExpireDateEnabled.value,
filesSharingPublicExpireDateDays = filesSharingPublicExpireDateDays,
filesSharingPublicExpireDateEnforced = filesSharingPublicExpireDateEnforced.value,
filesSharingPublicUpload = filesSharingPublicUpload.value,
filesSharingPublicMultiple = filesSharingPublicMultiple.value,
filesSharingPublicSupportsUploadOnly = filesSharingPublicSupportsUploadOnly.value,
filesSharingResharing = filesSharingResharing.value,
filesSharingFederationOutgoing = filesSharingFederationOutgoing.value,
filesSharingFederationIncoming = filesSharingFederationIncoming.value,
filesSharingUserProfilePicture = filesSharingUserProfilePicture.value,
filesBigFileChunking = filesBigFileChunking.value,
filesUndelete = filesUndelete.value,
filesVersioning = filesVersioning.value,
filesPrivateLinks = filesPrivateLinks.value,
appProviders = filesAppProviders,
tusSupport = filesTusSupport,
spaces = spaces,
passwordPolicy = passwordPolicy,
)
}
}
@@ -0,0 +1,44 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.datasources.implementation
import eu.qsfera.android.data.ClientManager
import eu.qsfera.android.data.capabilities.datasources.RemoteCapabilitiesDataSource
import eu.qsfera.android.data.capabilities.datasources.mapper.RemoteCapabilityMapper
import eu.qsfera.android.data.executeRemoteOperation
import eu.qsfera.android.domain.capabilities.model.OCCapability
class OCRemoteCapabilitiesDataSource(
private val clientManager: ClientManager,
private val remoteCapabilityMapper: RemoteCapabilityMapper
) : RemoteCapabilitiesDataSource {
override fun getCapabilities(
accountName: String
): OCCapability {
executeRemoteOperation {
clientManager.getCapabilityService(accountName).getCapabilities()
}.let { remoteCapability ->
return remoteCapabilityMapper.toModel(remoteCapability)!!.apply {
this.accountName = accountName
}
}
}
}
@@ -0,0 +1,139 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.datasources.mapper
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
import eu.qsfera.android.domain.capabilities.model.OCCapability
import eu.qsfera.android.domain.mappers.RemoteMapper
import eu.qsfera.android.lib.resources.status.RemoteCapability
import eu.qsfera.android.lib.resources.status.RemoteCapability.CapabilityBooleanType as RemoteCapabilityBooleanType
class RemoteCapabilityMapper : RemoteMapper<OCCapability, RemoteCapability> {
override fun toModel(remote: RemoteCapability?): OCCapability? =
remote?.let {
OCCapability(
accountName = remote.accountName,
versionMajor = remote.versionMajor,
versionMinor = remote.versionMinor,
versionMicro = remote.versionMicro,
versionString = remote.versionString,
versionEdition = remote.versionEdition,
corePollInterval = remote.corePollinterval,
davChunkingVersion = remote.chunkingVersion,
filesSharingApiEnabled = CapabilityBooleanType.fromValue(remote.filesSharingApiEnabled.value),
filesSharingPublicEnabled = CapabilityBooleanType.fromValue(remote.filesSharingPublicEnabled.value),
filesSharingPublicPasswordEnforced =
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforced.value),
filesSharingPublicPasswordEnforcedReadOnly =
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforcedReadOnly.value),
filesSharingPublicPasswordEnforcedReadWrite =
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforcedReadWrite.value),
filesSharingPublicPasswordEnforcedUploadOnly =
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforcedUploadOnly.value),
filesSharingPublicExpireDateEnabled =
CapabilityBooleanType.fromValue(remote.filesSharingPublicExpireDateEnabled.value),
filesSharingPublicExpireDateDays = remote.filesSharingPublicExpireDateDays,
filesSharingPublicExpireDateEnforced =
CapabilityBooleanType.fromValue(remote.filesSharingPublicExpireDateEnforced.value),
filesSharingPublicUpload = CapabilityBooleanType.fromValue(remote.filesSharingPublicUpload.value),
filesSharingPublicMultiple = CapabilityBooleanType.fromValue(remote.filesSharingPublicMultiple.value),
filesSharingPublicSupportsUploadOnly =
CapabilityBooleanType.fromValue(remote.filesSharingPublicSupportsUploadOnly.value),
filesSharingResharing = CapabilityBooleanType.fromValue(remote.filesSharingResharing.value),
filesSharingFederationOutgoing =
CapabilityBooleanType.fromValue(remote.filesSharingFederationOutgoing.value),
filesSharingFederationIncoming =
CapabilityBooleanType.fromValue(remote.filesSharingFederationIncoming.value),
filesSharingUserProfilePicture = CapabilityBooleanType.fromValue(remote.filesSharingUserProfilePicture.value),
filesBigFileChunking = CapabilityBooleanType.fromValue(remote.filesBigFileChunking.value),
filesUndelete = CapabilityBooleanType.fromValue(remote.filesUndelete.value),
filesVersioning = CapabilityBooleanType.fromValue(remote.filesVersioning.value),
filesPrivateLinks = CapabilityBooleanType.fromValue(remote.filesPrivateLinks.value),
filesAppProviders = remote.filesAppProviders?.firstOrNull()?.toAppProviders(),
filesTusSupport = remote.filesTusSupport?.toTusSupport(),
spaces = remote.spaces?.toSpaces(),
passwordPolicy = remote.passwordPolicy?.toPasswordPolicy()
)
}
override fun toRemote(model: OCCapability?): RemoteCapability? =
model?.let {
RemoteCapability(
accountName = model.accountName!!,
versionMajor = model.versionMajor,
versionMinor = model.versionMinor,
versionMicro = model.versionMicro,
versionString = model.versionString!!,
versionEdition = model.versionEdition!!,
chunkingVersion = model.davChunkingVersion,
corePollinterval = model.corePollInterval,
filesSharingApiEnabled = RemoteCapabilityBooleanType.fromValue(model.filesSharingApiEnabled.value)!!,
filesSharingPublicEnabled = RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicEnabled.value)!!,
filesSharingPublicPasswordEnforced =
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforced.value)!!,
filesSharingPublicPasswordEnforcedReadOnly =
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforcedReadOnly.value)!!,
filesSharingPublicPasswordEnforcedReadWrite =
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforcedReadWrite.value)!!,
filesSharingPublicPasswordEnforcedUploadOnly =
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforcedUploadOnly.value)!!,
filesSharingPublicExpireDateEnabled =
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicExpireDateEnabled.value)!!,
filesSharingPublicExpireDateDays = model.filesSharingPublicExpireDateDays,
filesSharingPublicExpireDateEnforced =
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicExpireDateEnforced.value)!!,
filesSharingPublicUpload = RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicUpload.value)!!,
filesSharingPublicMultiple = RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicMultiple.value)!!,
filesSharingPublicSupportsUploadOnly =
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicSupportsUploadOnly.value)!!,
filesSharingResharing = RemoteCapabilityBooleanType.fromValue(model.filesSharingResharing.value)!!,
filesSharingFederationOutgoing =
RemoteCapabilityBooleanType.fromValue(model.filesSharingFederationOutgoing.value)!!,
filesSharingFederationIncoming =
RemoteCapabilityBooleanType.fromValue(model.filesSharingFederationIncoming.value)!!,
filesSharingUserProfilePicture = RemoteCapabilityBooleanType.fromValue(model.filesSharingUserProfilePicture.value)!!,
filesBigFileChunking = RemoteCapabilityBooleanType.fromValue(model.filesBigFileChunking.value)!!,
filesUndelete = RemoteCapabilityBooleanType.fromValue(model.filesUndelete.value)!!,
filesVersioning = RemoteCapabilityBooleanType.fromValue(model.filesVersioning.value)!!,
filesPrivateLinks = RemoteCapabilityBooleanType.fromValue(model.filesPrivateLinks.value)!!,
filesAppProviders = null,
filesTusSupport = model.filesTusSupport?.toRemoteTusSupport(),
spaces = null,
passwordPolicy = null,
)
}
private fun RemoteCapability.RemoteAppProviders.toAppProviders() =
OCCapability.AppProviders(enabled, version, appsUrl, openUrl, openWebUrl, newUrl)
private fun RemoteCapability.TusSupport.toTusSupport() =
OCCapability.TusSupport(version, resumable, extension, maxChunkSize, httpMethodOverride)
private fun OCCapability.TusSupport.toRemoteTusSupport() =
RemoteCapability.TusSupport(version, resumable, extension, maxChunkSize, httpMethodOverride)
private fun RemoteCapability.RemoteSpaces.toSpaces() =
OCCapability.Spaces(enabled, projects, shareJail, hasMultiplePersonalSpaces)
private fun RemoteCapability.RemotePasswordPolicy.toPasswordPolicy() =
OCCapability.PasswordPolicy(maxCharacters, minCharacters, minDigits, minLowercaseCharacters, minSpecialCharacters, minUppercaseCharacters)
}
@@ -0,0 +1,73 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.db
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
@Dao
interface OCCapabilityDao {
@Query(SELECT)
fun getCapabilitiesForAccountAsLiveData(
accountName: String
): LiveData<OCCapabilityEntity?>
@Query(SELECT)
fun getCapabilitiesForAccount(
accountName: String
): OCCapabilityEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(ocCapability: OCCapabilityEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(ocCapabilities: List<OCCapabilityEntity>): List<Long>
@Query(DELETE_CAPABILITIES_BY_ACCOUNTNAME)
fun deleteByAccountName(accountName: String)
@Transaction
fun replace(ocCapabilities: List<OCCapabilityEntity>) {
ocCapabilities.forEach { ocCapability ->
ocCapability.accountName?.run {
deleteByAccountName(this)
}
}
insertOrReplace(ocCapabilities)
}
companion object {
private const val SELECT = """
SELECT *
FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}
WHERE ${ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME} = :accountName
"""
private const val DELETE_CAPABILITIES_BY_ACCOUNTNAME = """
DELETE
FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}
WHERE ${ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME} = :accountName
"""
}
}
@@ -0,0 +1,176 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.db
import android.database.Cursor
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_APP_PROVIDERS_PREFIX
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_DAV_CHUNKING_VERSION
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_BIGFILECHUNKING
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_PRIVATE_LINKS
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_UNDELETE
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_VERSIONING
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_PREFIX
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_PASSWORD_POLICY_PREFIX
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_API_ENABLED
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_INCOMING
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_OUTGOING
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ENABLED
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_MULTIPLE
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_UPLOAD
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_RESHARING
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_USER_PROFILE_PICTURE
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SPACES_PREFIX
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_EDITION
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MAJOR
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MICRO
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MINOR
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_STRING
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType.Companion.capabilityBooleanTypeUnknownString
import eu.qsfera.android.domain.capabilities.model.OCCapability
/**
* Represents one record of the Capabilities table.
*/
@Entity(tableName = CAPABILITIES_TABLE_NAME)
data class OCCapabilityEntity(
@ColumnInfo(name = CAPABILITIES_ACCOUNT_NAME)
val accountName: String?,
@ColumnInfo(name = CAPABILITIES_VERSION_MAJOR)
val versionMajor: Int,
@ColumnInfo(name = CAPABILITIES_VERSION_MINOR)
val versionMinor: Int,
@ColumnInfo(name = CAPABILITIES_VERSION_MICRO)
val versionMicro: Int,
@ColumnInfo(name = CAPABILITIES_VERSION_STRING)
val versionString: String?,
@ColumnInfo(name = CAPABILITIES_VERSION_EDITION)
val versionEdition: String?,
@ColumnInfo(name = CAPABILITIES_CORE_POLLINTERVAL)
val corePollInterval: Int,
@ColumnInfo(name = CAPABILITIES_DAV_CHUNKING_VERSION)
val davChunkingVersion: String,
@ColumnInfo(name = CAPABILITIES_SHARING_API_ENABLED, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingApiEnabled: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_ENABLED, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicEnabled: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicPasswordEnforced: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicPasswordEnforcedReadOnly: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicPasswordEnforcedReadWrite: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicPasswordEnforcedUploadOnly: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicExpireDateEnabled: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS)
val filesSharingPublicExpireDateDays: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicExpireDateEnforced: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_UPLOAD, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicUpload: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_MULTIPLE, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicMultiple: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingPublicSupportsUploadOnly: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_RESHARING, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingResharing: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_FEDERATION_OUTGOING, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingFederationOutgoing: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_FEDERATION_INCOMING, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingFederationIncoming: Int,
@ColumnInfo(name = CAPABILITIES_SHARING_USER_PROFILE_PICTURE, defaultValue = capabilityBooleanTypeUnknownString)
val filesSharingUserProfilePicture: Int,
@ColumnInfo(name = CAPABILITIES_FILES_BIGFILECHUNKING, defaultValue = capabilityBooleanTypeUnknownString)
val filesBigFileChunking: Int,
@ColumnInfo(name = CAPABILITIES_FILES_UNDELETE, defaultValue = capabilityBooleanTypeUnknownString)
val filesUndelete: Int,
@ColumnInfo(name = CAPABILITIES_FILES_VERSIONING, defaultValue = capabilityBooleanTypeUnknownString)
val filesVersioning: Int,
@ColumnInfo(name = CAPABILITIES_FILES_PRIVATE_LINKS, defaultValue = capabilityBooleanTypeUnknownString)
val filesPrivateLinks: Int,
@Embedded(prefix = CAPABILITIES_APP_PROVIDERS_PREFIX)
val appProviders: OCCapability.AppProviders?,
@Embedded(prefix = CAPABILITIES_TUS_SUPPORT_PREFIX)
val tusSupport: OCCapability.TusSupport?,
@Embedded(prefix = CAPABILITIES_SPACES_PREFIX)
val spaces: OCCapability.Spaces?,
@Embedded(prefix = CAPABILITIES_PASSWORD_POLICY_PREFIX)
val passwordPolicy: OCCapability.PasswordPolicy?,
) {
@PrimaryKey(autoGenerate = true) var id: Int = 0
companion object {
fun fromCursor(cursor: Cursor): OCCapabilityEntity = cursor.use {
OCCapabilityEntity(
it.getString(it.getColumnIndexOrThrow(CAPABILITIES_ACCOUNT_NAME)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_MAJOR)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_MINOR)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_MICRO)),
it.getString(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_STRING)),
it.getString(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_EDITION)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_CORE_POLLINTERVAL)),
it.getColumnIndex(CAPABILITIES_DAV_CHUNKING_VERSION).takeUnless { it < 0 }?.let { index -> it.getString(index) }.orEmpty(),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_API_ENABLED)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_ENABLED)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_UPLOAD)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_MULTIPLE)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_RESHARING)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_FEDERATION_OUTGOING)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_FEDERATION_INCOMING)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_USER_PROFILE_PICTURE)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_FILES_BIGFILECHUNKING)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_FILES_UNDELETE)),
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_FILES_VERSIONING)),
it.getColumnIndex(CAPABILITIES_FILES_PRIVATE_LINKS).takeUnless { it < 0 }?.let { index -> it.getInt(index) }
?: CapabilityBooleanType.UNKNOWN.value,
null,
null,
null,
null,
)
}
}
}
@@ -0,0 +1,55 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.capabilities.repository
import androidx.lifecycle.LiveData
import eu.qsfera.android.data.capabilities.datasources.LocalCapabilitiesDataSource
import eu.qsfera.android.data.capabilities.datasources.RemoteCapabilitiesDataSource
import eu.qsfera.android.domain.appregistry.AppRegistryRepository
import eu.qsfera.android.domain.capabilities.CapabilityRepository
import eu.qsfera.android.domain.capabilities.model.OCCapability
class OCCapabilityRepository(
private val localCapabilitiesDataSource: LocalCapabilitiesDataSource,
private val remoteCapabilitiesDataSource: RemoteCapabilitiesDataSource,
private val appRegistryRepository: AppRegistryRepository,
) : CapabilityRepository {
override fun getCapabilitiesAsLiveData(accountName: String): LiveData<OCCapability?> =
localCapabilitiesDataSource.getCapabilitiesForAccountAsLiveData(accountName)
override fun getStoredCapabilities(
accountName: String
): OCCapability? = localCapabilitiesDataSource.getCapabilitiesForAccount(accountName)
override fun refreshCapabilitiesForAccount(
accountName: String
) {
val capabilitiesFromNetwork = remoteCapabilitiesDataSource.getCapabilities(accountName)
localCapabilitiesDataSource.insertCapabilities(listOf(capabilitiesFromNetwork))
if (capabilitiesFromNetwork.filesAppProviders != null) {
appRegistryRepository.refreshAppRegistryForAccount(accountName)
}
}
}
@@ -0,0 +1,108 @@
/*
* qsfera Android client application
*
* @author Fernando Sanz Velasco
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.extensions
import timber.log.Timber
import java.io.File
import java.io.IOException
/**
* It's basically a copy of copyRecursively() but deleting the source file after copying it
* to the target location
*/
fun File.moveRecursively(
target: File,
overwrite: Boolean = false,
onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
): Boolean {
if (!exists()) {
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist.")) !=
OnErrorAction.TERMINATE
}
try {
// We cannot break for loop from inside a lambda, so we have to use an exception here
for (src in walkTopDown().onFail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
if (!src.exists()) {
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist.")) ==
OnErrorAction.TERMINATE
)
return false
} else {
val relPath = src.toRelativeString(this)
val dstFile = File(target, relPath)
if (dstFile.exists() && !(src.isDirectory && dstFile.isDirectory)) {
val stillExists = if (!overwrite) {
true
} else {
if (dstFile.isDirectory)
!dstFile.deleteRecursively()
else
!dstFile.delete()
}
if (stillExists) {
if (onError(
dstFile, FileAlreadyExistsException(
file = src,
other = dstFile,
reason = "The destination file already exists."
)
) == OnErrorAction.TERMINATE
)
return false
continue
}
}
if (src.isDirectory) {
dstFile.mkdirs()
} else {
try {
if (src.copyTo(dstFile, overwrite).length() != src.length()) {
if (onError(
src,
IOException("Source file wasn't copied completely, length of destination file differs.")
) == OnErrorAction.TERMINATE
)
return false
} else {
src.delete()
}
} catch (e: IOException) {
Timber.e(e, "An error occurred while trying to move the file")
src.delete()
dstFile.delete()
}
}
}
}
return true
} catch (e: TerminateException) {
Timber.e(e, "The process terminated unexpectedly")
return false
}
}
//Private exception class, used to terminate recursive copying.
private class TerminateException(file: File) : FileSystemException(file)
@@ -0,0 +1,67 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Christian Schabesberger
* @author Juan Carlos Garrote Gascón
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.datasources
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
import kotlinx.coroutines.flow.Flow
import java.util.UUID
interface LocalFileDataSource {
fun getFileById(fileId: Long): OCFile?
fun getFileByIdAsFlow(fileId: Long): Flow<OCFile?>
fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile?
fun getFileByRemoteId(remoteId: String): OCFile?
fun getFolderContent(folderId: Long): List<OCFile>
fun getSearchFolderContent(folderId: Long, search: String): List<OCFile>
fun getSearchAvailableOfflineFolderContent(folderId: Long, search: String): List<OCFile>
fun getSearchSharedByLinkFolderContent(folderId: Long, search: String): List<OCFile>
fun getFolderContentWithSyncInfoAsFlow(folderId: Long): Flow<List<OCFileWithSyncInfo>>
fun getFolderImages(folderId: Long): List<OCFile>
fun getSharedByLinkWithSyncInfoForAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>>
fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>>
fun getFilesAvailableOfflineFromAccount(owner: String): List<OCFile>
fun getFilesAvailableOfflineFromEveryAccount(): List<OCFile>
fun getDownloadedFilesForAccount(owner: String): List<OCFile>
fun getFileWithSyncInfoByIdAsFlow(id: Long): Flow<OCFileWithSyncInfo?>
fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFile>
fun moveFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, finalStoragePath: String)
fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String, replace: Boolean?)
fun saveFilesInFolderAndReturnTheFilesThatChanged(listOfFiles: List<OCFile>, folder: OCFile): List<OCFile>
fun saveFile(file: OCFile)
fun saveConflict(fileId: Long, eTagInConflict: String)
fun cleanConflict(fileId: Long)
fun deleteFile(fileId: Long)
fun deleteFilesForAccount(accountName: String)
fun renameFile(fileToRename: OCFile, finalRemotePath: String, finalStoragePath: String)
fun disableThumbnailsForFile(fileId: Long)
fun updateAvailableOfflineStatusForFile(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus)
fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String)
fun saveUploadWorkerUuid(fileId: Long, workerUuid: UUID)
fun saveDownloadWorkerUuid(fileId: Long, workerUuid: UUID)
fun cleanWorkersUuid(fileId: Long)
fun updateFileWithLastUsage(fileId: Long, lastUsage: Long?)
}
@@ -0,0 +1,99 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.datasources
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.files.model.OCMetaFile
interface RemoteFileDataSource {
fun checkPathExistence(
path: String,
isUserLogged: Boolean,
accountName: String,
spaceWebDavUrl: String?,
): Boolean
fun copyFile(
sourceRemotePath: String,
targetRemotePath: String,
accountName: String,
sourceSpaceWebDavUrl: String?,
targetSpaceWebDavUrl: String?,
replace: Boolean,
): String?
fun createFolder(
remotePath: String,
createFullPath: Boolean,
isChunksFolder: Boolean,
accountName: String,
spaceWebDavUrl: String?,
)
fun getAvailableRemotePath(
remotePath: String,
accountName: String,
spaceWebDavUrl: String?,
isUserLogged: Boolean,
): String
fun moveFile(
sourceRemotePath: String,
targetRemotePath: String,
accountName: String,
spaceWebDavUrl: String?,
replace: Boolean,
)
fun readFile(
remotePath: String,
accountName: String,
spaceWebDavUrl: String? = null,
): OCFile
fun refreshFolder(
remotePath: String,
accountName: String,
spaceWebDavUrl: String? = null,
): List<OCFile>
fun deleteFile(
remotePath: String,
accountName: String,
spaceWebDavUrl: String? = null,
)
fun renameFile(
oldName: String,
oldRemotePath: String,
newName: String,
isFolder: Boolean,
accountName: String,
spaceWebDavUrl: String? = null,
)
fun getMetaFile(
fileId: String,
accountName: String,
): OCMetaFile
}
@@ -0,0 +1,294 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.datasources.implementation
import androidx.annotation.VisibleForTesting
import eu.qsfera.android.data.files.datasources.LocalFileDataSource
import eu.qsfera.android.data.files.db.FileDao
import eu.qsfera.android.data.files.db.OCFileAndFileSync
import eu.qsfera.android.data.files.db.OCFileEntity
import eu.qsfera.android.data.spaces.datasources.implementation.OCLocalSpacesDataSource.Companion.toModel
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus
import eu.qsfera.android.domain.files.model.MIME_DIR
import eu.qsfera.android.domain.files.model.MIME_PREFIX_IMAGE
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PATH
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.UUID
class OCLocalFileDataSource(
private val fileDao: FileDao,
) : LocalFileDataSource {
override fun getFileById(fileId: Long): OCFile? =
fileDao.getFileById(fileId)?.toModel()
override fun getFileByIdAsFlow(fileId: Long): Flow<OCFile?> =
fileDao.getFileByIdAsFlow(fileId).map { it?.toModel() }
override fun getFileWithSyncInfoByIdAsFlow(id: Long): Flow<OCFileWithSyncInfo?> =
fileDao.getFileWithSyncInfoByIdAsFlow(id).map { it?.toModel() }
override fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile? {
fileDao.getFileByOwnerAndRemotePath(owner, remotePath, spaceId)?.let { return it.toModel() }
// If root folder do not exists, create and return it.
return if (remotePath == ROOT_PATH) {
val rootFolder = OCFile(
parentId = ROOT_PARENT_ID,
owner = owner,
remotePath = ROOT_PATH,
length = 0,
mimeType = MIME_DIR,
modificationTimestamp = 0,
spaceId = spaceId,
permissions = "CK",
)
val idFile = fileDao.mergeRemoteAndLocalFile(rootFolder.toEntity())
getFileById(idFile)
} else {
null
}
}
override fun getFileByRemoteId(remoteId: String): OCFile? =
fileDao.getFileByRemoteId(remoteId)?.toModel()
override fun getFolderContent(folderId: Long): List<OCFile> =
fileDao.getFolderContent(folderId = folderId).map {
it.toModel()
}
override fun getSearchFolderContent(folderId: Long, search: String): List<OCFile> =
fileDao.getSearchFolderContent(folderId = folderId, search = search).map {
it.toModel()
}
override fun getSearchAvailableOfflineFolderContent(folderId: Long, search: String): List<OCFile> =
fileDao.getSearchAvailableOfflineFolderContent(folderId = folderId, search = search).map {
it.toModel()
}
override fun getSearchSharedByLinkFolderContent(folderId: Long, search: String): List<OCFile> =
fileDao.getSearchSharedByLinkFolderContent(folderId = folderId, search = search).map {
it.toModel()
}
override fun getFolderContentWithSyncInfoAsFlow(folderId: Long): Flow<List<OCFileWithSyncInfo>> =
fileDao.getFolderContentWithSyncInfoAsFlow(folderId = folderId).map { folderContent ->
folderContent.map { it.toModel() }
}
override fun getFolderImages(folderId: Long): List<OCFile> =
fileDao.getFolderByMimeType(folderId = folderId, mimeType = MIME_PREFIX_IMAGE).map {
it.toModel()
}
override fun getSharedByLinkWithSyncInfoForAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
fileDao.getFilesWithSyncInfoSharedByLinkAsFlow(accountOwner = owner).map { fileList ->
fileList.map { it.toModel() }
}
override fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
fileDao.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner).map { fileList ->
fileList.map { it.toModel() }
}
override fun getFilesAvailableOfflineFromAccount(owner: String): List<OCFile> =
fileDao.getFilesAvailableOfflineFromAccount(accountOwner = owner).map {
it.toModel()
}
override fun getFilesAvailableOfflineFromEveryAccount(): List<OCFile> =
fileDao.getFilesAvailableOfflineFromEveryAccount().map {
it.toModel()
}
override fun getDownloadedFilesForAccount(owner: String): List<OCFile> =
fileDao.getDownloadedFilesForAccount(accountOwner = owner).map {
it.toModel()
}
override fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFile> =
fileDao.getFilesWithLastUsageOlderThanGivenTime(milliseconds).map {
it.toModel()
}
override fun moveFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, finalStoragePath: String) =
fileDao.moveFile(
sourceFile = sourceFile.toEntity(),
targetFolder = targetFolder.toEntity(),
finalRemotePath = finalRemotePath,
finalStoragePath = finalStoragePath
)
override fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String, replace: Boolean?) {
fileDao.copy(
sourceFile = sourceFile.toEntity(),
targetFolder = targetFolder.toEntity(),
finalRemotePath = finalRemotePath,
remoteId = remoteId,
replace = replace,
)
}
override fun saveFilesInFolderAndReturnTheFilesThatChanged(listOfFiles: List<OCFile>, folder: OCFile): List<OCFile> {
// To do: If it is root, add 0 as parent Id
val folderContent = fileDao.insertFilesInFolderAndReturnTheFilesThatChanged(
folder = folder.toEntity(),
folderContent = listOfFiles.map { it.toEntity() }
)
return folderContent.map { it.toModel() }
}
override fun saveFile(file: OCFile) {
fileDao.upsert(file.toEntity())
}
override fun saveConflict(fileId: Long, eTagInConflict: String) {
fileDao.updateConflictStatusForFile(fileId, eTagInConflict)
}
override fun cleanConflict(fileId: Long) {
fileDao.updateConflictStatusForFile(fileId, null)
}
override fun deleteFile(fileId: Long) {
fileDao.deleteFileById(fileId)
}
override fun deleteFilesForAccount(accountName: String) {
fileDao.deleteFilesForAccount(accountName)
}
override fun renameFile(fileToRename: OCFile, finalRemotePath: String, finalStoragePath: String) {
fileDao.moveFile(
sourceFile = fileToRename.toEntity(),
targetFolder = fileDao.getFileById(fileToRename.parentId!!)!!,
finalRemotePath = finalRemotePath,
finalStoragePath = finalStoragePath
)
}
override fun disableThumbnailsForFile(fileId: Long) {
fileDao.disableThumbnailsForFile(fileId)
}
override fun updateAvailableOfflineStatusForFile(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus) {
fileDao.updateAvailableOfflineStatusForFile(ocFile, newAvailableOfflineStatus.ordinal)
}
override fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String) {
fileDao.updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory, newDirectory)
}
override fun updateFileWithLastUsage(fileId: Long, lastUsage: Long?) {
fileDao.updateFileWithLastUsage(fileId, lastUsage)
}
override fun saveUploadWorkerUuid(fileId: Long, workerUuid: UUID) {
// Not yet implemented
}
override fun saveDownloadWorkerUuid(fileId: Long, workerUuid: UUID) {
fileDao.updateSyncStatusForFile(fileId, workerUuid)
}
override fun cleanWorkersUuid(fileId: Long) {
fileDao.updateSyncStatusForFile(fileId, null)
}
@VisibleForTesting
fun OCFileAndFileSync.toModel(): OCFileWithSyncInfo =
OCFileWithSyncInfo(
file = file.toModel(),
uploadWorkerUuid = fileSync?.uploadWorkerUuid,
downloadWorkerUuid = fileSync?.downloadWorkerUuid,
isSynchronizing = fileSync?.isSynchronizing == true,
space = space?.toModel(),
)
companion object {
@VisibleForTesting
fun OCFileEntity.toModel(): OCFile =
OCFile(
id = id,
parentId = parentId,
remotePath = remotePath,
owner = owner,
permissions = permissions,
remoteId = remoteId,
privateLink = privateLink,
creationTimestamp = creationTimestamp,
modificationTimestamp = modificationTimestamp,
etag = etag,
remoteEtag = remoteEtag,
mimeType = mimeType,
length = length,
sharedByLink = sharedByLink,
sharedWithSharee = sharedWithSharee,
storagePath = storagePath,
availableOfflineStatus = AvailableOfflineStatus.fromValue(availableOfflineStatus),
needsToUpdateThumbnail = needsToUpdateThumbnail,
fileIsDownloading = fileIsDownloading,
lastSyncDateForData = lastSyncDateForData,
lastUsage = lastUsage,
modifiedAtLastSyncForData = modifiedAtLastSyncForData,
etagInConflict = etagInConflict,
treeEtag = treeEtag,
spaceId = spaceId,
)
@VisibleForTesting
fun OCFile.toEntity(): OCFileEntity =
OCFileEntity(
parentId = parentId,
remotePath = remotePath,
owner = owner,
permissions = permissions,
remoteId = remoteId,
privateLink = privateLink,
creationTimestamp = creationTimestamp,
modificationTimestamp = modificationTimestamp,
etag = etag,
remoteEtag = remoteEtag,
mimeType = mimeType,
length = length,
sharedByLink = sharedByLink,
sharedWithSharee = sharedWithSharee,
storagePath = storagePath,
availableOfflineStatus = availableOfflineStatus?.ordinal ?: AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE.ordinal,
needsToUpdateThumbnail = needsToUpdateThumbnail,
fileIsDownloading = fileIsDownloading,
lastSyncDateForData = lastSyncDateForData,
lastUsage = lastUsage,
modifiedAtLastSyncForData = modifiedAtLastSyncForData,
etagInConflict = etagInConflict,
treeEtag = treeEtag,
name = fileName,
spaceId = spaceId,
).apply { this@toEntity.id?.let { modelId -> this.id = modelId } }
}
}
@@ -0,0 +1,251 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
* @author Manuel Plazas Palacio
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.datasources.implementation
import androidx.annotation.VisibleForTesting
import eu.qsfera.android.data.ClientManager
import eu.qsfera.android.data.executeRemoteOperation
import eu.qsfera.android.data.files.datasources.RemoteFileDataSource
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.files.model.OCMetaFile
import eu.qsfera.android.lib.resources.files.RemoteFile
import eu.qsfera.android.lib.resources.files.RemoteMetaFile
class OCRemoteFileDataSource(
private val clientManager: ClientManager,
) : RemoteFileDataSource {
override fun checkPathExistence(
path: String,
isUserLogged: Boolean,
accountName: String,
spaceWebDavUrl: String?,
): Boolean = clientManager.getFileService(accountName).checkPathExistence(
path = path,
isUserLogged = isUserLogged,
spaceWebDavUrl = spaceWebDavUrl,
).data
override fun copyFile(
sourceRemotePath: String,
targetRemotePath: String,
accountName: String,
sourceSpaceWebDavUrl: String?,
targetSpaceWebDavUrl: String?,
replace: Boolean,
): String? = executeRemoteOperation {
clientManager.getFileService(accountName).copyFile(
sourceRemotePath = sourceRemotePath,
targetRemotePath = targetRemotePath,
sourceSpaceWebDavUrl = sourceSpaceWebDavUrl,
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
replace = replace,
)
}
override fun createFolder(
remotePath: String,
createFullPath: Boolean,
isChunksFolder: Boolean,
accountName: String,
spaceWebDavUrl: String?,
) = executeRemoteOperation {
clientManager.getFileService(accountName).createFolder(
remotePath = remotePath,
createFullPath = createFullPath,
isChunkFolder = isChunksFolder,
spaceWebDavUrl = spaceWebDavUrl,
)
}
/**
* Checks if remotePath does not exist in the server and returns it, or adds
* a suffix to it in order to avoid the server file is overwritten.
*
* @param remotePath
* @return
*/
override fun getAvailableRemotePath(
remotePath: String,
accountName: String,
spaceWebDavUrl: String?,
isUserLogged: Boolean,
): String {
var checkExistsFile = checkPathExistence(
path = remotePath,
isUserLogged = isUserLogged,
accountName = accountName,
spaceWebDavUrl = spaceWebDavUrl,
)
if (!checkExistsFile) {
return remotePath
}
val pos = remotePath.lastIndexOf(".")
var suffix: String
var extension = ""
if (pos >= 0) {
extension = remotePath.substring(pos + 1)
remotePath.apply {
substring(0, pos)
}
}
var count = 1
do {
suffix = " ($count)"
checkExistsFile = if (pos >= 0) {
checkPathExistence(
path = "${remotePath.substringBeforeLast('.', "")}$suffix.$extension",
isUserLogged = isUserLogged,
accountName = accountName,
spaceWebDavUrl = spaceWebDavUrl,
)
} else {
checkPathExistence(
path = "$remotePath$suffix",
isUserLogged = isUserLogged,
accountName = accountName,
spaceWebDavUrl = spaceWebDavUrl,
)
}
count++
} while (checkExistsFile)
return if (pos >= 0) {
"${remotePath.substringBeforeLast('.', "")}$suffix.$extension"
} else {
remotePath + suffix
}
}
override fun moveFile(
sourceRemotePath: String,
targetRemotePath: String,
accountName: String,
spaceWebDavUrl: String?,
replace: Boolean,
) = executeRemoteOperation {
clientManager.getFileService(accountName).moveFile(
sourceRemotePath = sourceRemotePath,
targetRemotePath = targetRemotePath,
spaceWebDavUrl = spaceWebDavUrl,
replace = replace,
)
}
override fun readFile(
remotePath: String,
accountName: String,
spaceWebDavUrl: String?,
): OCFile = executeRemoteOperation {
clientManager.getFileService(accountName).readFile(
remotePath = remotePath,
spaceWebDavUrl = spaceWebDavUrl,
)
}.toModel()
override fun refreshFolder(
remotePath: String,
accountName: String,
spaceWebDavUrl: String?,
): List<OCFile> =
// Assert not null, service should return an empty list if no files there.
executeRemoteOperation {
clientManager.getFileService(accountName).refreshFolder(
remotePath = remotePath,
spaceWebDavUrl = spaceWebDavUrl,
)
}.let { listOfRemote ->
listOfRemote.map { remoteFile -> remoteFile.toModel() }
}
override fun deleteFile(
remotePath: String,
accountName: String,
spaceWebDavUrl: String?,
) = executeRemoteOperation {
clientManager.getFileService(accountName).removeFile(
remotePath = remotePath,
spaceWebDavUrl = spaceWebDavUrl,
)
}
override fun renameFile(
oldName: String,
oldRemotePath: String,
newName: String,
isFolder: Boolean,
accountName: String,
spaceWebDavUrl: String?,
) = executeRemoteOperation {
clientManager.getFileService(accountName).renameFile(
oldName = oldName,
oldRemotePath = oldRemotePath,
newName = newName,
isFolder = isFolder,
spaceWebDavUrl = spaceWebDavUrl,
)
}
override fun getMetaFile(
fileId: String,
accountName: String,
): OCMetaFile = executeRemoteOperation {
clientManager.getFileService(accountName).getMetaFileInfo(fileId)
}.toModel()
companion object {
@VisibleForTesting
fun RemoteFile.toModel(): OCFile =
OCFile(
owner = owner,
remoteId = remoteId,
remotePath = if (isFolder && !remotePath.endsWith(OCFile.PATH_SEPARATOR)) {
"$remotePath${OCFile.PATH_SEPARATOR}"
} else {
remotePath
},
length = if (isFolder) {
size
} else {
length
},
creationTimestamp = creationTimestamp,
modificationTimestamp = modifiedTimestamp,
mimeType = mimeType,
etag = etag,
remoteEtag = etag,
permissions = permissions,
privateLink = privateLink,
sharedWithSharee = sharedWithSharee,
sharedByLink = sharedByLink,
)
@VisibleForTesting
fun RemoteMetaFile.toModel(): OCMetaFile =
OCMetaFile(
path = metaPathForUser,
id = id,
fileId = fileId,
spaceId = spaceId,
)
}
}
@@ -0,0 +1,619 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.Upsert
import eu.qsfera.android.data.ProviderMeta
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE_PARENT
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE
import eu.qsfera.android.domain.extensions.isOneOf
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID
import kotlinx.coroutines.flow.Flow
import java.io.File.separatorChar
import java.util.UUID
@Dao
interface FileDao {
@Query(SELECT_FILE_WITH_ID)
fun getFileById(
id: Long
): OCFileEntity?
@Query(SELECT_FILE_WITH_ID)
fun getFileByIdAsFlow(
id: Long
): Flow<OCFileEntity?>
@Transaction
@Query(SELECT_FILE_WITH_ID)
fun getFileWithSyncInfoById(
id: Long
): OCFileAndFileSync?
@Transaction
@Query(SELECT_FILE_WITH_ID)
fun getFileWithSyncInfoByIdAsFlow(
id: Long
): Flow<OCFileAndFileSync?>
@Query(SELECT_FILE_FROM_OWNER_WITH_REMOTE_PATH)
fun getFileByOwnerAndRemotePath(
owner: String,
remotePath: String,
spaceId: String?,
): OCFileEntity?
@Query(SELECT_FILE_WITH_REMOTE_ID)
fun getFileByRemoteId(
remoteId: String
): OCFileEntity?
@Query(SELECT_FILTERED_FOLDER_CONTENT)
fun getSearchFolderContent(
folderId: Long,
search: String
): List<OCFileEntity>
@Query(SELECT_FILTERED_AVAILABLE_OFFLINE_FOLDER_CONTENT)
fun getSearchAvailableOfflineFolderContent(
folderId: Long,
search: String
): List<OCFileEntity>
@Query(SELECT_FILTERED_SHARED_BY_LINK_FOLDER_CONTENT)
fun getSearchSharedByLinkFolderContent(
folderId: Long,
search: String
): List<OCFileEntity>
@Query(SELECT_FOLDER_CONTENT)
fun getFolderContent(
folderId: Long
): List<OCFileEntity>
@Transaction
@Query(SELECT_FOLDER_CONTENT)
fun getFolderContentWithSyncInfo(
folderId: Long
): List<OCFileAndFileSync>
@Transaction
@Query(SELECT_FOLDER_CONTENT)
fun getFolderContentWithSyncInfoAsFlow(
folderId: Long
): Flow<List<OCFileAndFileSync>>
@Query(SELECT_FOLDER_BY_MIMETYPE)
fun getFolderByMimeType(
folderId: Long,
mimeType: String
): List<OCFileEntity>
@Transaction
@Query(SELECT_FILES_SHARED_BY_LINK)
fun getFilesWithSyncInfoSharedByLinkAsFlow(
accountOwner: String
): Flow<List<OCFileAndFileSync>>
@Transaction
@Query(SELECT_FILES_AVAILABLE_OFFLINE_FROM_ACCOUNT)
fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(
accountOwner: String
): Flow<List<OCFileAndFileSync>>
@Query(SELECT_FILES_AVAILABLE_OFFLINE_FROM_ACCOUNT)
fun getFilesAvailableOfflineFromAccount(
accountOwner: String
): List<OCFileEntity>
@Query(SELECT_FILES_AVAILABLE_OFFLINE_FROM_EVERY_ACCOUNT)
fun getFilesAvailableOfflineFromEveryAccount(): List<OCFileEntity>
@Query(SELECT_DOWNLOADED_FILES_FOR_ACCOUNT)
fun getDownloadedFilesForAccount(
accountOwner: String
): List<OCFileEntity>
@Query(SELECT_FILES_WHERE_LAST_USAGE_IS_OLDER_THAN_GIVEN_TIME)
fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFileEntity>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertOrIgnore(ocFileEntity: OCFileEntity): Long
@Update
fun updateFile(ocFileEntity: OCFileEntity)
@Upsert
fun upsert(ocFileEntity: OCFileEntity)
@Transaction
fun updateSyncStatusForFile(id: Long, workerUuid: UUID?) {
val fileWithSyncInfoEntity = getFileWithSyncInfoById(id)
if ((fileWithSyncInfoEntity?.file?.parentId != ROOT_PARENT_ID) &&
((workerUuid == null) != (fileWithSyncInfoEntity?.fileSync?.downloadWorkerUuid == null))
) {
val fileSyncEntity = if (workerUuid == null) {
OCFileSyncEntity(
fileId = id,
uploadWorkerUuid = null,
downloadWorkerUuid = null,
isSynchronizing = false
)
} else {
OCFileSyncEntity(
fileId = id,
uploadWorkerUuid = null,
downloadWorkerUuid = workerUuid,
isSynchronizing = true
)
}
insertOrReplaceFileSync(fileSyncEntity)
// Check if there is any more file synchronizing in this folder, in such case don't update parent's sync status
var cleanSyncInParent = true
if (workerUuid == null) {
val folderContent = getFolderContentWithSyncInfo(fileWithSyncInfoEntity?.file?.parentId!!)
var indexFileInFolder = 0
while (cleanSyncInParent && indexFileInFolder < folderContent.size) {
val child = folderContent[indexFileInFolder]
if (child.fileSync?.isSynchronizing == true) {
cleanSyncInParent = false
}
indexFileInFolder++
}
}
if (workerUuid != null || cleanSyncInParent) {
updateSyncStatusForFile(fileWithSyncInfoEntity?.file?.parentId!!, workerUuid)
}
}
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplaceFileSync(ocFileSyncEntity: OCFileSyncEntity): Long
/**
* Make sure that the ids are set properly. We don't take care of conflicts and that stuff here.
*
* return folder content
*/
@Transaction
fun insertFilesInFolderAndReturnTheFilesThatChanged(
folder: OCFileEntity,
folderContent: List<OCFileEntity>,
): List<OCFileEntity> {
var folderId = insertOrIgnore(folder)
// If it was already in database
if (folderId == -1L) {
updateFile(folder)
folderId = folder.id
}
folderContent.forEach { fileToInsert ->
upsert(fileToInsert.apply {
parentId = folderId
availableOfflineStatus = getNewAvailableOfflineStatus(folder.availableOfflineStatus, fileToInsert.availableOfflineStatus)
})
}
val folderContentLocal = getFolderContent(folderId)
return folderContentLocal.filter { localFile ->
folderContent.any { changedFile ->
localFile.remoteId == changedFile.remoteId
}
}
}
@Transaction
fun mergeRemoteAndLocalFile(
ocFileEntity: OCFileEntity
): Long {
val localFile: OCFileEntity? = getFileByOwnerAndRemotePath(
owner = ocFileEntity.owner,
remotePath = ocFileEntity.remotePath,
ocFileEntity.spaceId,
)
return if (localFile == null) {
insertOrIgnore(ocFileEntity)
} else {
insertOrIgnore(ocFileEntity.copy(
parentId = localFile.parentId,
lastSyncDateForData = localFile.lastSyncDateForData,
modifiedAtLastSyncForData = localFile.modifiedAtLastSyncForData,
storagePath = localFile.storagePath,
treeEtag = localFile.treeEtag,
etagInConflict = localFile.etagInConflict,
availableOfflineStatus = localFile.availableOfflineStatus,
lastUsage = localFile.lastUsage,
).apply {
id = localFile.id
})
}
}
@Transaction
fun copy(
sourceFile: OCFileEntity,
targetFolder: OCFileEntity,
finalRemotePath: String,
remoteId: String?,
replace: Boolean?,
) {
// 1. Update target size
upsert(
targetFolder.copy(
length = targetFolder.length + sourceFile.length
).apply { id = targetFolder.id }
)
if (replace == true) {
remoteId?.let { deleteFileByRemoteId(it) }
}
// 2. Insert a new file with common attributes and retrieved remote id
upsert(
OCFileEntity(
parentId = targetFolder.id,
owner = targetFolder.owner,
remotePath = finalRemotePath,
remoteId = remoteId,
length = sourceFile.length,
modificationTimestamp = sourceFile.modificationTimestamp,
mimeType = sourceFile.mimeType,
name = null,
needsToUpdateThumbnail = true,
etag = "",
creationTimestamp = null,
permissions = null,
treeEtag = "",
availableOfflineStatus = NOT_AVAILABLE_OFFLINE.ordinal,
)
)
}
@Transaction
fun moveFile(
sourceFile: OCFileEntity,
targetFolder: OCFileEntity,
finalRemotePath: String,
finalStoragePath: String
) {
// 1. Update target size
upsert(
targetFolder.copy(
length = targetFolder.length + sourceFile.length
).apply { id = targetFolder.id }
)
// 2. Update source
if (sourceFile.isFolder) {
// Update remote path and storage path when moving a folder
moveFolder(
sourceFolder = sourceFile,
targetFolder = targetFolder,
targetRemotePath = finalRemotePath,
targetStoragePath = finalStoragePath
)
} else {
// Update remote path, storage path, parent file when moving a file
moveSingleFile(
sourceFile = sourceFile,
targetFolder = targetFolder,
finalRemotePath = finalRemotePath,
finalStoragePath = sourceFile.storagePath?.let { finalStoragePath }
)
}
}
@Query(DELETE_FILE_WITH_ID)
fun deleteFileById(id: Long)
@Query(DELETE_FILE_WITH_REMOTE_ID)
fun deleteFileByRemoteId(remoteId: String)
@Query(UPDATE_FILES_STORAGE_DIRECTORY)
fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String)
@Transaction
fun updateAvailableOfflineStatusForFile(ocFile: OCFile, newAvailableOfflineStatus: Int) {
if (ocFile.isFolder) {
updateFolderWithNewAvailableOfflineStatus(ocFile.id!!, newAvailableOfflineStatus)
} else {
updateFileWithAvailableOfflineStatus(ocFile.id!!, newAvailableOfflineStatus)
}
}
private fun updateFolderWithNewAvailableOfflineStatus(ocFolderId: Long, newAvailableOfflineStatus: Int) {
updateFileWithAvailableOfflineStatus(ocFolderId, newAvailableOfflineStatus)
val newStatusForChildren = if (newAvailableOfflineStatus == NOT_AVAILABLE_OFFLINE.ordinal) {
NOT_AVAILABLE_OFFLINE.ordinal
} else {
AVAILABLE_OFFLINE_PARENT.ordinal
}
val folderContent = getFolderContent(ocFolderId)
folderContent.forEach { folderChild ->
if (folderChild.isFolder) {
updateFolderWithNewAvailableOfflineStatus(folderChild.id, newStatusForChildren)
} else {
updateFileWithAvailableOfflineStatus(folderChild.id, newStatusForChildren)
}
}
}
@Query(UPDATE_FILE_WITH_NEW_AVAILABLE_OFFLINE_STATUS)
fun updateFileWithAvailableOfflineStatus(id: Long, availableOfflineStatus: Int)
@Query(UPDATE_FILE_WITH_LAST_USAGE)
fun updateFileWithLastUsage(id: Long, lastUsage: Long?)
@Transaction
fun updateConflictStatusForFile(id: Long, eTagInConflict: String?) {
val fileEntity = getFileById(id)
if (fileEntity?.parentId != ROOT_PARENT_ID) {
updateFileWithConflictStatus(id, eTagInConflict)
// Check if there is any more file with conflicts in this folder, in such case don't update parent's conflict status
var cleanConflictInParent = true
if (eTagInConflict == null) {
val folderContent = getFolderContent(fileEntity?.parentId!!)
var indexFileInFolder = 0
while (cleanConflictInParent && indexFileInFolder < folderContent.size) {
val child = folderContent[indexFileInFolder]
if (child.etagInConflict != null) {
cleanConflictInParent = false
}
indexFileInFolder++
}
}
if (eTagInConflict != null || cleanConflictInParent) {
updateConflictStatusForFile(fileEntity?.parentId!!, eTagInConflict)
}
}
}
@Query(UPDATE_FILE_WITH_NEW_CONFLICT_STATUS)
fun updateFileWithConflictStatus(id: Long, eTagInConflict: String?)
@Query(DISABLE_THUMBNAILS_FOR_FILE)
fun disableThumbnailsForFile(fileId: Long)
@Query(DELETE_FILES_FOR_ACCOUNT)
fun deleteFilesForAccount(accountName: String)
private fun moveSingleFile(
sourceFile: OCFileEntity,
targetFolder: OCFileEntity,
finalRemotePath: String,
finalStoragePath: String?
) {
upsert(
sourceFile.copy(
parentId = targetFolder.id,
remotePath = finalRemotePath,
storagePath = finalStoragePath,
availableOfflineStatus = getNewAvailableOfflineStatus(targetFolder.availableOfflineStatus, sourceFile.availableOfflineStatus)
).apply { id = sourceFile.id }
)
}
private fun moveFolder(
sourceFolder: OCFileEntity,
targetFolder: OCFileEntity,
targetRemotePath: String,
targetStoragePath: String?
) {
// 1. Move the folder
val folderRemotePath =
targetRemotePath.trimEnd(separatorChar).plus(separatorChar)
val folderStoragePath =
targetStoragePath?.trimEnd(separatorChar)?.plus(separatorChar)
moveSingleFile(
sourceFile = sourceFolder,
targetFolder = targetFolder,
finalRemotePath = folderRemotePath,
finalStoragePath = sourceFolder.storagePath?.let { folderStoragePath }
)
// 2. Move its content
val folderContent = getFolderContent(sourceFolder.id)
folderContent.forEach { file ->
val remotePathForChild = folderRemotePath.plus(file.name)
val storagePathForChild = folderStoragePath?.plus(file.name)
if (file.isFolder) {
moveFolder(
sourceFolder = file,
targetFolder = sourceFolder,
targetRemotePath = remotePathForChild,
targetStoragePath = storagePathForChild
)
} else {
moveSingleFile(
sourceFile = file,
targetFolder = sourceFolder,
finalRemotePath = remotePathForChild,
finalStoragePath = storagePathForChild
)
}
}
}
/**
* If folder is available offline, the child gets the AVAILABLE_OFFLINE_PARENT status
* If child was available offline because of the previous parent, it won't be av offline anymore
* Otherwise, keep the child available offline status
*/
private fun getNewAvailableOfflineStatus(
parentFolderAvailableOfflineStatus: Int?,
currentFileAvailableOfflineStatus: Int?,
): Int =
if ((parentFolderAvailableOfflineStatus != null) &&
parentFolderAvailableOfflineStatus.isOneOf(AVAILABLE_OFFLINE.ordinal, AVAILABLE_OFFLINE_PARENT.ordinal)
) {
AVAILABLE_OFFLINE_PARENT.ordinal
} else if (currentFileAvailableOfflineStatus == AVAILABLE_OFFLINE.ordinal) {
AVAILABLE_OFFLINE.ordinal
} else {
NOT_AVAILABLE_OFFLINE.ordinal
}
companion object {
private const val SELECT_FILE_WITH_ID = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE id = :id
"""
private const val SELECT_FILE_WITH_REMOTE_ID = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE remoteId = :remoteId
"""
private const val SELECT_FILE_FROM_OWNER_WITH_REMOTE_PATH = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE owner = :owner AND remotePath = :remotePath AND spaceId IS :spaceId
"""
private const val DELETE_FILE_WITH_ID = """
DELETE
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE id = :id
"""
private const val DELETE_FILE_WITH_REMOTE_ID = """
DELETE
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE remoteId = :remoteId
"""
private const val SELECT_FOLDER_CONTENT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE parentId = :folderId
"""
private const val SELECT_FILTERED_FOLDER_CONTENT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE parentId = :folderId AND remotePath LIKE '%' || :search || '%'
"""
private const val SELECT_FILTERED_AVAILABLE_OFFLINE_FOLDER_CONTENT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE parentId = :folderId AND keepInSync = '1' AND remotePath LIKE '%' || :search || '%'
"""
private const val SELECT_FILTERED_SHARED_BY_LINK_FOLDER_CONTENT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE parentId = :folderId AND remotePath LIKE '%' || :search || '%' AND sharedByLink LIKE '%1%'
"""
private const val SELECT_FOLDER_BY_MIMETYPE = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE parentId = :folderId AND mimeType LIKE :mimeType || '%'
"""
private const val SELECT_DOWNLOADED_FILES_FOR_ACCOUNT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE owner = :accountOwner AND storagePath IS NOT NULL AND keepInSync = '0'
"""
private const val SELECT_FILES_SHARED_BY_LINK = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE owner = :accountOwner AND sharedByLink LIKE '%1%'
"""
private const val SELECT_FILES_AVAILABLE_OFFLINE_FROM_ACCOUNT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE owner = :accountOwner AND keepInSync = '1'
"""
private const val SELECT_FILES_AVAILABLE_OFFLINE_FROM_EVERY_ACCOUNT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE keepInSync = '1'
"""
private const val SELECT_FILES_WHERE_LAST_USAGE_IS_OLDER_THAN_GIVEN_TIME = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE lastUsage < (strftime('%s', 'now') * 1000 - :milliseconds)
AND keepInSync = '0'
"""
private const val UPDATE_FILE_WITH_NEW_AVAILABLE_OFFLINE_STATUS = """
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
SET keepInSync = :availableOfflineStatus
WHERE id = :id
"""
private const val UPDATE_FILE_WITH_NEW_CONFLICT_STATUS = """
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
SET etagInConflict = :eTagInConflict
WHERE id = :id
"""
private const val UPDATE_FILE_WITH_LAST_USAGE = """
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
SET lastUsage = :lastUsage
WHERE id = :id
"""
private const val DISABLE_THUMBNAILS_FOR_FILE = """
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
SET needsToUpdateThumbnail = false
WHERE id = :fileId
"""
private const val UPDATE_FILES_STORAGE_DIRECTORY = """
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
SET storagePath = `REPLACE`(storagePath, :oldDirectory, :newDirectory)
WHERE storagePath IS NOT NULL
"""
private const val DELETE_FILES_FOR_ACCOUNT = """
DELETE
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
WHERE owner = :accountName
"""
}
}
@@ -0,0 +1,39 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.db
import androidx.room.Embedded
import androidx.room.Relation
import eu.qsfera.android.data.spaces.db.SpacesEntity
data class OCFileAndFileSync(
@Embedded val file: OCFileEntity,
@Relation(
parentColumn = "id",
entityColumn = "fileId"
)
val fileSync: OCFileSyncEntity?,
@Relation(
parentColumn = "spaceId",
entityColumn = "space_id"
)
val space: SpacesEntity? = null,
)
@@ -0,0 +1,143 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.db
import android.database.Cursor
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_ACCOUNT_OWNER
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_CONTENT_LENGTH
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_CONTENT_TYPE
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_CREATION
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_ETAG
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_ETAG_IN_CONFLICT
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_REMOTE_ETAG
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_IS_DOWNLOADING
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_KEEP_IN_SYNC
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_MODIFIED
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_OWNER
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PARENT
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PATH
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PERMISSIONS
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PRIVATE_LINK
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_REMOTE_ID
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_SHARED_VIA_LINK
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_SHARED_WITH_SHAREE
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_SPACE_ID
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_STORAGE_PATH
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_TREE_ETAG
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_UPDATE_THUMBNAIL
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta._ID
import eu.qsfera.android.data.spaces.db.SpacesEntity
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID
import eu.qsfera.android.domain.extensions.isOneOf
import eu.qsfera.android.domain.files.model.MIME_DIR
import eu.qsfera.android.domain.files.model.MIME_DIR_UNIX
@Entity(
tableName = FILES_TABLE_NAME,
foreignKeys = [ForeignKey(
entity = SpacesEntity::class,
parentColumns = arrayOf(SPACES_ACCOUNT_NAME, SPACES_ID),
childColumns = arrayOf(FILE_OWNER, FILE_SPACE_ID),
onDelete = ForeignKey.CASCADE
)]
)
data class OCFileEntity(
var parentId: Long? = null,
val owner: String,
val remotePath: String,
val remoteId: String?,
val length: Long,
val creationTimestamp: Long?,
val modificationTimestamp: Long,
val mimeType: String,
val etag: String?,
val remoteEtag: String? = null,
val permissions: String?,
val privateLink: String? = null,
val storagePath: String? = null,
var name: String? = null,
val treeEtag: String? = null,
@ColumnInfo(name = "keepInSync")
var availableOfflineStatus: Int? = null,
val lastSyncDateForData: Long? = null,
val lastUsage: Long? = null,
val fileShareViaLink: Int? = null,
var needsToUpdateThumbnail: Boolean = false,
val modifiedAtLastSyncForData: Long? = null,
val etagInConflict: String? = null,
val fileIsDownloading: Boolean? = null,
val sharedWithSharee: Boolean? = false,
var sharedByLink: Boolean = false,
val spaceId: String? = null,
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
/**
* Use this to find out if this file is a folder.
*
* @return true if it is a folder
*/
val isFolder
get() = mimeType.isOneOf(MIME_DIR, MIME_DIR_UNIX)
companion object {
fun fromCursor(cursor: Cursor): OCFileEntity =
OCFileEntity(
parentId = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_PARENT)),
remotePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH)),
owner = cursor.getString(cursor.getColumnIndexOrThrow(FILE_ACCOUNT_OWNER)),
permissions = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PERMISSIONS)),
remoteId = cursor.getString(cursor.getColumnIndexOrThrow(FILE_REMOTE_ID)),
privateLink = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PRIVATE_LINK)),
creationTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_CREATION)),
modificationTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_MODIFIED)),
etag = cursor.getString(cursor.getColumnIndexOrThrow(FILE_ETAG)),
remoteEtag = cursor.getString(cursor.getColumnIndexOrThrow(FILE_REMOTE_ETAG)),
mimeType = cursor.getStringFromColumnOrEmpty(FILE_CONTENT_TYPE),
length = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_CONTENT_LENGTH)),
storagePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_STORAGE_PATH)),
name = cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
treeEtag = cursor.getString(cursor.getColumnIndexOrThrow(FILE_TREE_ETAG)),
lastSyncDateForData = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_LAST_SYNC_DATE_FOR_DATA)),
availableOfflineStatus = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_KEEP_IN_SYNC)),
fileShareViaLink = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_SHARED_VIA_LINK)),
needsToUpdateThumbnail = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_UPDATE_THUMBNAIL)) == 1,
modifiedAtLastSyncForData = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA)),
etagInConflict = cursor.getString(cursor.getColumnIndexOrThrow(FILE_ETAG_IN_CONFLICT)),
fileIsDownloading = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_IS_DOWNLOADING)) == 1,
sharedWithSharee = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_SHARED_WITH_SHAREE)) == 1
).apply {
id = cursor.getLong(cursor.getColumnIndexOrThrow(_ID))
}
private fun Cursor.getStringFromColumnOrEmpty(
columnName: String
): String = getColumnIndex(columnName).takeUnless { it < 0 }?.let { getString(it) }.orEmpty()
}
}
@@ -0,0 +1,43 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.db
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILES_SYNC_TABLE_NAME
import java.util.UUID
@Entity(
tableName = FILES_SYNC_TABLE_NAME,
foreignKeys = [ForeignKey(
entity = OCFileEntity::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("fileId"),
onDelete = ForeignKey.CASCADE
)]
)
data class OCFileSyncEntity(
@PrimaryKey val fileId: Long,
val uploadWorkerUuid: UUID?,
val downloadWorkerUuid: UUID?,
val isSynchronizing: Boolean
)
@@ -0,0 +1,619 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Christian Schabesberger
* @author Juan Carlos Garrote Gascón
* @author Manuel Plazas Palacio
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.files.repository
import eu.qsfera.android.data.files.datasources.LocalFileDataSource
import eu.qsfera.android.data.files.datasources.RemoteFileDataSource
import eu.qsfera.android.data.providers.LocalStorageProvider
import eu.qsfera.android.data.spaces.datasources.LocalSpacesDataSource
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE_PARENT
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE
import eu.qsfera.android.domain.exceptions.ConflictException
import eu.qsfera.android.domain.exceptions.FileAlreadyExistsException
import eu.qsfera.android.domain.exceptions.FileNotFoundException
import eu.qsfera.android.domain.files.FileRepository
import eu.qsfera.android.domain.files.model.FileListOption
import eu.qsfera.android.domain.files.model.MIME_DIR
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.files.model.OCFile.Companion.PATH_SEPARATOR
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PATH
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
import java.io.File
import java.util.UUID
class OCFileRepository(
private val localFileDataSource: LocalFileDataSource,
private val remoteFileDataSource: RemoteFileDataSource,
private val localSpacesDataSource: LocalSpacesDataSource,
private val localStorageProvider: LocalStorageProvider,
) : FileRepository {
override fun createFolder(
remotePath: String,
parentFolder: OCFile,
) {
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(parentFolder.spaceId, parentFolder.owner)
remoteFileDataSource.createFolder(
remotePath = remotePath,
createFullPath = false,
isChunksFolder = false,
accountName = parentFolder.owner,
spaceWebDavUrl = spaceWebDavUrl,
).also {
localFileDataSource.saveFilesInFolderAndReturnTheFilesThatChanged(
folder = parentFolder,
listOfFiles = listOf(
OCFile(
remotePath = remotePath,
owner = parentFolder.owner,
modificationTimestamp = System.currentTimeMillis(),
length = 0,
mimeType = MIME_DIR,
spaceId = parentFolder.spaceId,
permissions = "CK" // To be able to write inside a folder before the fetch is done
)
)
)
}
}
override fun copyFile(listOfFilesToCopy: List<OCFile>, targetFolder: OCFile, replace: List<Boolean?>, isUserLogged: Boolean): List<OCFile> {
val sourceSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(listOfFilesToCopy[0].spaceId, listOfFilesToCopy[0].owner)
val targetSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(targetFolder.spaceId, targetFolder.owner)
val filesNeedAction = mutableListOf<OCFile>()
listOfFilesToCopy.forEachIndexed forEach@{ position, ocFile ->
// 1. Get the final remote path for this file.
val expectedRemotePath: String = targetFolder.remotePath + ocFile.fileName
val finalRemotePath: String? =
getFinalRemotePath(
replace = replace,
expectedRemotePath = expectedRemotePath,
targetFolder = targetFolder,
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
filesNeedsAction = filesNeedAction,
ocFile = ocFile,
position = position,
isUserLogged = isUserLogged,
)
if (finalRemotePath != null && (replace.isEmpty() || replace[position] != null)) {
// 2. Try to copy files in server
val remoteId = try {
remoteFileDataSource.copyFile(
sourceRemotePath = ocFile.remotePath,
targetRemotePath = finalRemotePath,
accountName = ocFile.owner,
sourceSpaceWebDavUrl = sourceSpaceWebDavUrl,
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
replace = if (replace.isEmpty()) false else replace[position]!!,
)
} catch (targetNodeDoesNotExist: ConflictException) {
// Target node does not exist anymore. Remove target folder from database and local storage and return
deleteLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false)
throw targetNodeDoesNotExist
} catch (sourceFileDoesNotExist: FileNotFoundException) {
// Source file does not exist anymore. Remove file from database and local storage and continue
if (ocFile.isFolder) {
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false)
} else {
deleteLocalFile(
ocFile = ocFile,
onlyFromLocalStorage = false
)
}
if (listOfFilesToCopy.size == 1) {
throw sourceFileDoesNotExist
} else {
return@forEach
}
}
// 3. Update database with latest changes
remoteId?.let {
localFileDataSource.copyFile(
sourceFile = ocFile,
targetFolder = targetFolder,
finalRemotePath = finalRemotePath,
remoteId = it,
replace = if (replace.isEmpty()) {
null
} else {
replace[position]
},
)
}
}
}
return filesNeedAction
}
override fun getFileById(fileId: Long): OCFile? =
localFileDataSource.getFileById(fileId)
override fun getFileByIdAsFlow(fileId: Long): Flow<OCFile?> =
localFileDataSource.getFileByIdAsFlow(fileId)
override fun getFileWithSyncInfoByIdAsFlow(fileId: Long): Flow<OCFileWithSyncInfo?> =
localFileDataSource.getFileWithSyncInfoByIdAsFlow(fileId)
override fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile? =
localFileDataSource.getFileByRemotePath(remotePath, owner, spaceId)
override fun getFileFromRemoteId(fileId: String, accountName: String): OCFile? {
val metaFile = remoteFileDataSource.getMetaFile(fileId, accountName)
val remotePath = metaFile.path!!
val splitPath = remotePath.split(PATH_SEPARATOR)
var containerFolder = listOf<OCFile>()
for (i in 0..splitPath.size - 2) {
var path = splitPath[0]
for (j in 1..i) {
path += "$PATH_SEPARATOR${splitPath[j]}"
}
containerFolder = refreshFolder(path, accountName, metaFile.spaceId)
}
refreshFolder(remotePath, accountName, metaFile.spaceId)
return if (remotePath == ROOT_PATH) {
getFileByRemotePath(remotePath, accountName, metaFile.spaceId)
} else {
containerFolder.find { file ->
if (file.isFolder) {
file.remotePath.dropLast(1)
} else {
file.remotePath
} == remotePath
}
}
}
override fun getPersonalRootFolderForAccount(owner: String): OCFile {
val personalSpace = localSpacesDataSource.getPersonalSpaceForAccount(owner)
if (personalSpace == null) {
val legacyRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = null)
try {
return legacyRootFolder ?: throw IllegalStateException("LegacyRootFolder not found")
} catch (e: IllegalStateException) {
Timber.i("There was an error: $e")
}
}
// To do: Retrieving the root folders should return a non nullable. If they don't exist yet, they are created and returned. Remove nullability
val personalRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = personalSpace?.root?.id)
return personalRootFolder!!
}
override fun getSharesRootFolderForAccount(owner: String): OCFile? {
val sharesSpaces = localSpacesDataSource.getSharesSpaceForAccount(owner) ?: return null
val personalRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = sharesSpaces.root.id)
return personalRootFolder!!
}
override fun getSearchFolderContent(fileListOption: FileListOption, folderId: Long, search: String): List<OCFile> =
when (fileListOption) {
FileListOption.ALL_FILES -> localFileDataSource.getSearchFolderContent(folderId, search)
FileListOption.SPACES_LIST -> emptyList()
FileListOption.AV_OFFLINE -> localFileDataSource.getSearchAvailableOfflineFolderContent(folderId, search)
FileListOption.SHARED_BY_LINK -> localFileDataSource.getSearchSharedByLinkFolderContent(folderId, search)
}
override fun getFolderContent(folderId: Long): List<OCFile> =
localFileDataSource.getFolderContent(folderId)
override fun getFolderContentWithSyncInfoAsFlow(folderId: Long): Flow<List<OCFileWithSyncInfo>> =
localFileDataSource.getFolderContentWithSyncInfoAsFlow(folderId)
override fun getFolderImages(folderId: Long): List<OCFile> =
localFileDataSource.getFolderImages(folderId)
override fun getSharedByLinkWithSyncInfoForAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
localFileDataSource.getSharedByLinkWithSyncInfoForAccountAsFlow(owner)
override fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
localFileDataSource.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner)
override fun getFilesAvailableOfflineFromAccount(owner: String): List<OCFile> =
localFileDataSource.getFilesAvailableOfflineFromAccount(owner)
override fun getFilesAvailableOfflineFromEveryAccount(): List<OCFile> =
localFileDataSource.getFilesAvailableOfflineFromEveryAccount()
override fun getDownloadedFilesForAccount(owner: String): List<OCFile> = localFileDataSource.getDownloadedFilesForAccount(owner)
override fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFile> =
localFileDataSource.getFilesWithLastUsageOlderThanGivenTime(milliseconds)
override fun moveFile(listOfFilesToMove: List<OCFile>, targetFolder: OCFile, replace: List<Boolean?>, isUserLogged: Boolean): List<OCFile> {
val targetSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(targetFolder.spaceId, targetFolder.owner)
val filesNeedsAction = mutableListOf<OCFile>()
listOfFilesToMove.forEachIndexed forEach@{ position, ocFile ->
// 1. Get the final remote path for this file.
val expectedRemotePath: String = targetFolder.remotePath + ocFile.fileName
val finalRemotePath: String? =
getFinalRemotePath(
replace = replace,
expectedRemotePath = expectedRemotePath,
targetFolder = targetFolder,
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
filesNeedsAction = filesNeedsAction,
ocFile = ocFile,
position = position,
isUserLogged = isUserLogged,
)
if (finalRemotePath != null && (replace.isEmpty() || replace[position] != null)) {
val finalStoragePath: String = localStorageProvider.getDefaultSavePathFor(targetFolder.owner, finalRemotePath, targetFolder.spaceId)
// 2. Try to move files in server
try {
remoteFileDataSource.moveFile(
sourceRemotePath = ocFile.remotePath,
targetRemotePath = finalRemotePath,
accountName = ocFile.owner,
spaceWebDavUrl = targetSpaceWebDavUrl,
replace = if (replace.isEmpty()) false else replace[position]!!,
)
} catch (targetNodeDoesNotExist: ConflictException) {
// Target node does not exist anymore. Remove target folder from database and local storage and return
deleteLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false)
throw targetNodeDoesNotExist
} catch (sourceFileDoesNotExist: FileNotFoundException) {
// Source file does not exist anymore. Remove file from database and local storage and continue
if (ocFile.isFolder) {
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false)
} else {
deleteLocalFile(
ocFile = ocFile,
onlyFromLocalStorage = false
)
}
if (listOfFilesToMove.size == 1) {
throw sourceFileDoesNotExist
} else {
return@forEach
}
}
// 3. Clean conflict in old location if there was a conflict
ocFile.etagInConflict?.let {
localFileDataSource.cleanConflict(ocFile.id!!)
}
// 4. Update database with latest changes
localFileDataSource.moveFile(
sourceFile = ocFile,
targetFolder = targetFolder,
finalRemotePath = finalRemotePath,
finalStoragePath = finalStoragePath
)
// 5. Save conflict in new location if there was conflict
ocFile.etagInConflict?.let {
localFileDataSource.saveConflict(ocFile.id!!, it)
}
// 6. Update local storage
localStorageProvider.moveLocalFile(ocFile, finalStoragePath)
}
}
return filesNeedsAction
}
override fun readFile(remotePath: String, accountName: String, spaceId: String?): OCFile {
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(spaceId, accountName)
return remoteFileDataSource.readFile(remotePath, accountName, spaceWebDavUrl).copy(spaceId = spaceId)
}
override fun refreshFolder(
remotePath: String,
accountName: String,
spaceId: String?,
isActionSetFolderAvailableOfflineOrSynchronize: Boolean,
): List<OCFile> {
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(spaceId, accountName)
// Retrieve remote folder data
val fetchFolderResult = remoteFileDataSource.refreshFolder(remotePath, accountName, spaceWebDavUrl).map {
it.copy(spaceId = spaceId)
}
val remoteFolder = fetchFolderResult.first()
val remoteFolderContent = fetchFolderResult.drop(1).distinctBy { it.remotePath }
// Final content for this folder, we will update the folder content all together
val folderContentUpdated = mutableListOf<OCFile>()
// Check if the folder already exists in database.
val localFolderByRemotePath: OCFile? =
localFileDataSource.getFileByRemotePath(remotePath = remoteFolder.remotePath, owner = remoteFolder.owner, spaceId = spaceId)
// If folder doesn't exists in database, insert everything. Easy path
if (localFolderByRemotePath == null) {
folderContentUpdated.addAll(remoteFolderContent.map { it.apply { needsToUpdateThumbnail = !it.isFolder } })
} else {
// Keep the current local properties or we will miss relevant things.
remoteFolder.copyLocalPropertiesFrom(localFolderByRemotePath)
// Folder already exists in database, get database content to update files accordingly
val localFolderContent = localFileDataSource.getFolderContent(folderId = localFolderByRemotePath.id!!)
val localFilesMap = localFolderContent.associateBy { localFile -> localFile.remoteId ?: localFile.remotePath }.toMutableMap()
// Loop to sync every child
remoteFolderContent.forEach { remoteChild ->
// Let's try with remote path if the file does not have remote id yet
val localChildToSync = localFilesMap.remove(remoteChild.remoteId) ?: localFilesMap.remove(remoteChild.remotePath)
// If local child does not exists, just insert the new one.
if (localChildToSync == null) {
folderContentUpdated.add(
remoteChild.apply {
parentId = localFolderByRemotePath.id
needsToUpdateThumbnail = !remoteChild.isFolder
// remote eTag will not be set unless file CONTENTS are synchronized
etag = ""
availableOfflineStatus =
if (remoteFolder.isAvailableOffline) AVAILABLE_OFFLINE_PARENT else NOT_AVAILABLE_OFFLINE
})
} else if (localChildToSync.etag != remoteChild.etag ||
localChildToSync.localModificationTimestamp > remoteChild.lastSyncDateForData!! ||
isActionSetFolderAvailableOfflineOrSynchronize
) {
// File exists in the database, we need to check several stuff.
folderContentUpdated.add(
remoteChild.apply {
copyLocalPropertiesFrom(localChildToSync)
// DO NOT update etag till contents are synced.
etag = localChildToSync.etag
needsToUpdateThumbnail =
(!remoteChild.isFolder && remoteChild.modificationTimestamp != localChildToSync.modificationTimestamp) ||
localChildToSync.needsToUpdateThumbnail
// Probably not needed, if the child was already in the database, the av offline status should be also there
if (remoteFolder.isAvailableOffline) {
availableOfflineStatus = AVAILABLE_OFFLINE_PARENT
}
// Fix: What about renames? Need to fix storage path
})
}
}
// Remaining items should be removed from the database and local storage. They do not exists in remote anymore.
localFilesMap.map { it.value }.forEach { ocFile ->
ocFile.etagInConflict?.let {
localFileDataSource.cleanConflict(ocFile.id!!)
}
if (ocFile.isFolder) {
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false)
} else {
deleteLocalFile(ocFile = ocFile, onlyFromLocalStorage = false)
}
}
}
val anyConflictInThisFolder = folderContentUpdated.any { it.etagInConflict != null }
if (!anyConflictInThisFolder) {
remoteFolder.etagInConflict = null
}
return localFileDataSource.saveFilesInFolderAndReturnTheFilesThatChanged(
folder = remoteFolder,
listOfFiles = folderContentUpdated
)
}
override fun deleteFiles(listOfFilesToDelete: List<OCFile>, removeOnlyLocalCopy: Boolean) {
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(
spaceId = listOfFilesToDelete.first().spaceId,
accountName = listOfFilesToDelete.first().owner,
)
listOfFilesToDelete.forEach { ocFile ->
if (!removeOnlyLocalCopy) {
try {
remoteFileDataSource.deleteFile(
remotePath = ocFile.remotePath,
accountName = ocFile.owner,
spaceWebDavUrl = spaceWebDavUrl,
)
} catch (fileNotFoundException: FileNotFoundException) {
Timber.i(fileNotFoundException, "File ${ocFile.fileName} was not found in server. Let's remove it from local storage")
}
}
ocFile.etagInConflict?.let {
localFileDataSource.cleanConflict(ocFile.id!!)
}
if (ocFile.isFolder) {
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = removeOnlyLocalCopy)
} else {
deleteLocalFile(ocFile = ocFile, onlyFromLocalStorage = removeOnlyLocalCopy)
}
}
}
override fun renameFile(ocFile: OCFile, newName: String) {
// 1. Compose new remote path
val newRemotePath = localStorageProvider.getExpectedRemotePath(
remotePath = ocFile.remotePath,
newName = newName,
isFolder = ocFile.isFolder
)
// 2. Check if file already exists in database
if (localFileDataSource.getFileByRemotePath(newRemotePath, ocFile.owner, ocFile.spaceId) != null) {
throw FileAlreadyExistsException()
}
// 3. Retrieve the specific web dav url in case there is one.
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(
spaceId = ocFile.spaceId,
accountName = ocFile.owner,
)
// 4. Perform remote operation
remoteFileDataSource.renameFile(
oldName = ocFile.fileName,
oldRemotePath = ocFile.remotePath,
newName = newName,
isFolder = ocFile.isFolder,
accountName = ocFile.owner,
spaceWebDavUrl = spaceWebDavUrl,
)
// 5. Save new remote path in the local database
localFileDataSource.renameFile(
fileToRename = ocFile,
finalRemotePath = newRemotePath,
finalStoragePath = localStorageProvider.getDefaultSavePathFor(ocFile.owner, newRemotePath, ocFile.spaceId)
)
// 6. Update local storage
localStorageProvider.moveLocalFile(
ocFile = ocFile,
finalStoragePath = localStorageProvider.getDefaultSavePathFor(ocFile.owner, newRemotePath, ocFile.spaceId)
)
}
override fun saveFile(file: OCFile) {
localFileDataSource.saveFile(file)
}
override fun saveConflict(fileId: Long, eTagInConflict: String) {
localFileDataSource.saveConflict(fileId, eTagInConflict)
}
override fun cleanConflict(fileId: Long) {
localFileDataSource.cleanConflict(fileId)
}
override fun disableThumbnailsForFile(fileId: Long) {
localFileDataSource.disableThumbnailsForFile(fileId)
}
override fun updateFileWithNewAvailableOfflineStatus(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus) {
localFileDataSource.updateAvailableOfflineStatusForFile(ocFile, newAvailableOfflineStatus)
}
override fun updateFileWithLastUsage(fileId: Long, lastUsage: Long?) {
localFileDataSource.updateFileWithLastUsage(fileId, lastUsage)
}
override fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String) {
localFileDataSource.updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory, newDirectory)
}
override fun saveDownloadWorkerUuid(fileId: Long, workerUuid: UUID) {
localFileDataSource.saveDownloadWorkerUuid(fileId, workerUuid)
}
override fun cleanWorkersUuid(fileId: Long) {
localFileDataSource.cleanWorkersUuid(fileId)
}
private fun getFinalRemotePath(
replace: List<Boolean?>,
expectedRemotePath: String,
targetFolder: OCFile,
targetSpaceWebDavUrl: String?,
filesNeedsAction: MutableList<OCFile>,
ocFile: OCFile,
position: Int,
isUserLogged: Boolean,
) =
if (replace.isEmpty()) {
val pathExists = remoteFileDataSource.checkPathExistence(
path = expectedRemotePath,
isUserLogged = isUserLogged,
accountName = targetFolder.owner,
spaceWebDavUrl = targetSpaceWebDavUrl,
)
if (pathExists) {
filesNeedsAction.add(ocFile)
null
} else {
if (ocFile.isFolder) expectedRemotePath.plus(File.separator) else expectedRemotePath
}
} else {
if (replace[position] == true) {
if (ocFile.isFolder) expectedRemotePath.plus(File.separator) else expectedRemotePath
} else if (replace[position] == false) {
remoteFileDataSource.getAvailableRemotePath(
remotePath = expectedRemotePath,
accountName = targetFolder.owner,
spaceWebDavUrl = targetSpaceWebDavUrl,
isUserLogged = isUserLogged,
).let {
if (ocFile.isFolder) it.plus(File.separator) else it
}
} else {
null
}
}
private fun deleteLocalFolderRecursively(ocFile: OCFile, onlyFromLocalStorage: Boolean) {
val folderContent = localFileDataSource.getFolderContent(ocFile.id!!)
// 1. Remove folder content recursively
folderContent.forEach { file ->
// The condition will not be met when onlyFromLocalStorage is true and the file is of type available offline
if (!(onlyFromLocalStorage && file.isAvailableOffline)) {
if (file.isFolder) {
deleteLocalFolderRecursively(ocFile = file, onlyFromLocalStorage = onlyFromLocalStorage)
} else {
deleteLocalFile(ocFile = file, onlyFromLocalStorage = onlyFromLocalStorage)
}
}
}
// 2. Remove the folder itself if it has no files
deleteLocalFolderIfItHasNoFilesInside(ocFolder = ocFile, onlyFromLocalStorage = onlyFromLocalStorage)
}
private fun deleteLocalFolderIfItHasNoFilesInside(ocFolder: OCFile, onlyFromLocalStorage: Boolean) {
localStorageProvider.deleteLocalFolderIfItHasNoFilesInside(ocFolder = ocFolder)
deleteOrResetFileFromDatabase(ocFolder, onlyFromLocalStorage)
}
private fun deleteLocalFile(ocFile: OCFile, onlyFromLocalStorage: Boolean) {
localStorageProvider.deleteLocalFile(ocFile)
deleteOrResetFileFromDatabase(ocFile, onlyFromLocalStorage)
}
private fun deleteOrResetFileFromDatabase(ocFile: OCFile, onlyFromLocalStorage: Boolean) {
if (onlyFromLocalStorage) {
localFileDataSource.saveFile(ocFile.copy(storagePath = null, etagInConflict = null, lastUsage = null, etag = null))
} else {
localFileDataSource.deleteFile(ocFile.id!!)
}
}
}
@@ -0,0 +1,33 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.folderbackup.datasources
import eu.qsfera.android.domain.automaticuploads.model.AutomaticUploadsConfiguration
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
import kotlinx.coroutines.flow.Flow
interface LocalFolderBackupDataSource {
fun getAutomaticUploadsConfiguration(): AutomaticUploadsConfiguration?
fun getFolderBackupConfigurationByNameAsFlow(name: String): Flow<FolderBackUpConfiguration?>
fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration)
fun resetFolderBackupConfigurationByName(name: String)
}
@@ -0,0 +1,92 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.folderbackup.datasources.implementation
import androidx.annotation.VisibleForTesting
import eu.qsfera.android.data.folderbackup.datasources.LocalFolderBackupDataSource
import eu.qsfera.android.data.folderbackup.db.FolderBackUpEntity
import eu.qsfera.android.data.folderbackup.db.FolderBackupDao
import eu.qsfera.android.domain.automaticuploads.model.AutomaticUploadsConfiguration
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.videoUploadsName
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class OCLocalFolderBackupDataSource(
private val folderBackupDao: FolderBackupDao,
) : LocalFolderBackupDataSource {
override fun getAutomaticUploadsConfiguration(): AutomaticUploadsConfiguration? {
val pictureUploadsConfiguration = folderBackupDao.getFolderBackUpConfigurationByName(pictureUploadsName)
val videoUploadsConfiguration = folderBackupDao.getFolderBackUpConfigurationByName(videoUploadsName)
if (pictureUploadsConfiguration == null && videoUploadsConfiguration == null) return null
return AutomaticUploadsConfiguration(
pictureUploadsConfiguration = pictureUploadsConfiguration?.toModel(),
videoUploadsConfiguration = videoUploadsConfiguration?.toModel(),
)
}
override fun getFolderBackupConfigurationByNameAsFlow(name: String): Flow<FolderBackUpConfiguration?> =
folderBackupDao.getFolderBackUpConfigurationByNameAsFlow(name = name).map { it?.toModel() }
override fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) {
folderBackupDao.update(folderBackUpConfiguration.toEntity())
}
override fun resetFolderBackupConfigurationByName(name: String) {
folderBackupDao.delete(name)
}
/**************************************************************************************************************
************************************************* Mappers ****************************************************
**************************************************************************************************************/
private fun FolderBackUpConfiguration.toEntity(): FolderBackUpEntity =
FolderBackUpEntity(
accountName = accountName,
behavior = behavior.toString(),
sourcePath = sourcePath,
uploadPath = uploadPath,
wifiOnly = wifiOnly,
chargingOnly = chargingOnly,
name = name,
lastSyncTimestamp = lastSyncTimestamp,
spaceId = spaceId,
)
companion object {
@VisibleForTesting
fun FolderBackUpEntity.toModel() =
FolderBackUpConfiguration(
accountName = accountName,
behavior = UploadBehavior.fromString(behavior),
sourcePath = sourcePath,
uploadPath = uploadPath,
wifiOnly = wifiOnly,
chargingOnly = chargingOnly,
lastSyncTimestamp = lastSyncTimestamp,
name = name,
spaceId = spaceId,
)
}
}
@@ -0,0 +1,38 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.folderbackup.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import eu.qsfera.android.data.ProviderMeta
@Entity(tableName = ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME)
data class FolderBackUpEntity(
val accountName: String,
val behavior: String,
val sourcePath: String,
val uploadPath: String,
val wifiOnly: Boolean,
val chargingOnly: Boolean,
val name: String,
val lastSyncTimestamp: Long,
val spaceId: String?,
) {
@PrimaryKey(autoGenerate = true) var id: Int = 0
}
@@ -0,0 +1,67 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.folderbackup.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import eu.qsfera.android.data.ProviderMeta
import kotlinx.coroutines.flow.Flow
@Dao
interface FolderBackupDao {
@Query(SELECT)
fun getFolderBackUpConfigurationByName(
name: String
): FolderBackUpEntity?
@Query(SELECT)
fun getFolderBackUpConfigurationByNameAsFlow(
name: String
): Flow<FolderBackUpEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(folderBackUpEntity: FolderBackUpEntity): Long
@Query(DELETE)
fun delete(name: String): Int
@Transaction
fun update(folderBackUpEntity: FolderBackUpEntity): Long {
delete(folderBackUpEntity.name)
return insertOrReplace(folderBackUpEntity)
}
companion object {
private const val SELECT = """
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME}
WHERE name = :name
"""
private const val DELETE = """
DELETE
FROM ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME}
WHERE name = :name
"""
}
}
@@ -0,0 +1,44 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.folderbackup.repository
import eu.qsfera.android.data.folderbackup.datasources.LocalFolderBackupDataSource
import eu.qsfera.android.domain.automaticuploads.FolderBackupRepository
import eu.qsfera.android.domain.automaticuploads.model.AutomaticUploadsConfiguration
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
import kotlinx.coroutines.flow.Flow
class OCFolderBackupRepository(
private val localFolderBackupDataSource: LocalFolderBackupDataSource
) : FolderBackupRepository {
override fun getAutomaticUploadsConfiguration(): AutomaticUploadsConfiguration? =
localFolderBackupDataSource.getAutomaticUploadsConfiguration()
override fun getFolderBackupConfigurationByNameAsFlow(name: String): Flow<FolderBackUpConfiguration?> =
localFolderBackupDataSource.getFolderBackupConfigurationByNameAsFlow(name)
override fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) {
localFolderBackupDataSource.saveFolderBackupConfiguration(folderBackUpConfiguration)
}
override fun resetFolderBackupConfigurationByName(name: String) =
localFolderBackupDataSource.resetFolderBackupConfigurationByName(name)
}
@@ -0,0 +1,69 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.migrations
import androidx.room.DeleteColumn
import androidx.room.RenameColumn
import androidx.room.migration.AutoMigrationSpec
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MAJOR
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME
@RenameColumn(
tableName = CAPABILITIES_TABLE_NAME,
fromColumnName = "version_mayor",
toColumnName = CAPABILITIES_VERSION_MAJOR
)
@RenameColumn(
tableName = CAPABILITIES_TABLE_NAME,
fromColumnName = "enabled",
toColumnName = "enabledAppProviders"
)
@RenameColumn(
tableName = CAPABILITIES_TABLE_NAME,
fromColumnName = "version",
toColumnName = "versionAppProviders"
)
@RenameColumn(
tableName = CAPABILITIES_TABLE_NAME,
fromColumnName = "appsUrl",
toColumnName = "appsUrlAppProviders"
)
@RenameColumn(
tableName = CAPABILITIES_TABLE_NAME,
fromColumnName = "openUrl",
toColumnName = "openUrlAppProviders"
)
@RenameColumn(
tableName = CAPABILITIES_TABLE_NAME,
fromColumnName = "openWebUrl",
toColumnName = "openWebUrlAppProviders"
)
@RenameColumn(
tableName = CAPABILITIES_TABLE_NAME,
fromColumnName = "newUrl",
toColumnName = "newUrlAppProviders"
)
@DeleteColumn(
tableName = FILES_TABLE_NAME,
columnName = "lastSyncDateForProperties"
)
class AutoMigration39To40 : AutoMigrationSpec
@@ -0,0 +1,61 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
val MIGRATION_27_28 = object : Migration(27, 28) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
execSQL(
"CREATE TABLE IF NOT EXISTS `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL," +
" `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `sharing_api_enabled`" +
" INTEGER NOT NULL DEFAULT -1, `search_min_length` INTEGER, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1," +
" `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only`" +
" INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1," +
" `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled`" +
" INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced`" +
" INTEGER NOT NULL DEFAULT -1, `sharing_public_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER" +
" NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL" +
" DEFAULT -1, `sharing_user_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1," +
" `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1," +
" `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning`" +
" INTEGER NOT NULL DEFAULT -1)"
)
execSQL("ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} ADD COLUMN search_min_length INTEGER")
execSQL(
"INSERT INTO `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` SELECT id, account, version_mayor, version_minor, version_micro," +
" version_string, version_edition,core_pollinterval, IFNULL(sharing_api_enabled, -1), search_min_length," +
" IFNULL(sharing_public_enabled, -1), IFNULL(sharing_public_password_enforced, -1)," +
"IFNULL(sharing_public_password_enforced_read_only, -1),IFNULL(sharing_public_password_enforced_read_write, -1)," +
"IFNULL(sharing_public_password_enforced_public_only, -1),IFNULL(sharing_public_expire_date_enabled, -1)," +
"IFNULL(sharing_public_expire_date_days, 0),IFNULL(sharing_public_expire_date_enforced, -1)," +
"IFNULL(sharing_public_send_mail, -1),IFNULL(sharing_public_upload, -1),IFNULL(sharing_public_multiple, -1)," +
" IFNULL(supports_upload_only, -1), IFNULL(sharing_user_send_mail, -1),IFNULL(sharing_resharing, -1)," +
"IFNULL(sharing_federation_outgoing, -1),IFNULL(sharing_federation_incoming, -1),IFNULL(files_bigfilechunking, -1)," +
"IFNULL(files_undelete, -1),IFNULL(files_versioning, -1) FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}"
)
execSQL("DROP TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
execSQL("ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2 RENAME TO ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
}
}
}
@@ -0,0 +1,70 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
val MIGRATION_28_29 = object : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
execSQL("DROP TABLE ${ProviderTableMeta.OCSHARES_TABLE_NAME}")
execSQL(
"CREATE TABLE IF NOT EXISTS `${ProviderTableMeta.OCSHARES_TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" `file_source` TEXT NOT NULL, `item_source` TEXT NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT," +
" `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER" +
" NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER" +
" NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL," +
" `name` TEXT, `url` TEXT)"
)
execSQL(
"CREATE TABLE IF NOT EXISTS `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL," +
" `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `sharing_api_enabled` INTEGER NOT" +
" NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT" +
" NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1," +
" `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1," +
" `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled`" +
" INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced`" +
" INTEGER NOT NULL DEFAULT -1, `sharing_public_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT" +
" NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT" +
" -1, `sharing_user_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1," +
" `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1," +
" `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning`" +
" INTEGER NOT NULL DEFAULT -1)"
)
execSQL(
"INSERT INTO `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` SELECT id, account, version_mayor, version_minor, version_micro," +
" version_string, version_edition, core_pollinterval, IFNULL(sharing_api_enabled, -1), IFNULL(sharing_public_enabled, -1)," +
" IFNULL(sharing_public_password_enforced, -1),IFNULL(sharing_public_password_enforced_read_only, -1)," +
"IFNULL(sharing_public_password_enforced_read_write, -1),IFNULL(sharing_public_password_enforced_public_only, -1)," +
"IFNULL(sharing_public_expire_date_enabled, -1),IFNULL(sharing_public_expire_date_days, 0)," +
"IFNULL(sharing_public_expire_date_enforced, -1),IFNULL(sharing_public_send_mail, -1),IFNULL(sharing_public_upload, -1)," +
"IFNULL(sharing_public_multiple, -1), IFNULL(supports_upload_only, -1), IFNULL(sharing_user_send_mail, -1)," +
"IFNULL(sharing_resharing, -1),IFNULL(sharing_federation_outgoing, -1),IFNULL(sharing_federation_incoming, -1)," +
"IFNULL(files_bigfilechunking, -1),IFNULL(files_undelete, -1),IFNULL(files_versioning, -1) " +
"FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}"
)
execSQL("DROP TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
execSQL("ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2 RENAME TO ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
}
}
}
@@ -0,0 +1,62 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta
val MIGRATION_29_30 = object : Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
execSQL(
"CREATE TABLE IF NOT EXISTS `${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
" NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER" +
" NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version`" +
" TEXT NOT NULL DEFAULT '',`sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL" +
" DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only`" +
" INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1," +
" `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER" +
" NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER" +
" NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL" +
" DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1," +
" `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1," +
" `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning`" +
" INTEGER NOT NULL DEFAULT -1)"
)
execSQL(
"INSERT INTO `${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` SELECT id, account, version_mayor, version_minor," +
" version_micro, version_string, version_edition, core_pollinterval, '', IFNULL(sharing_api_enabled, -1)," +
" IFNULL(sharing_public_enabled, -1), IFNULL(sharing_public_password_enforced, -1)," +
"IFNULL(sharing_public_password_enforced_read_only, -1),IFNULL(sharing_public_password_enforced_read_write, -1)," +
"IFNULL(sharing_public_password_enforced_public_only, -1),IFNULL(sharing_public_expire_date_enabled, -1)," +
"IFNULL(sharing_public_expire_date_days, 0),IFNULL(sharing_public_expire_date_enforced, -1)," +
" IFNULL(sharing_public_upload, -1),IFNULL(sharing_public_multiple, -1), IFNULL(supports_upload_only, -1)," +
" IFNULL(sharing_resharing, -1),IFNULL(sharing_federation_outgoing, -1),IFNULL(sharing_federation_incoming, -1)," +
"IFNULL(files_bigfilechunking, -1),IFNULL(files_undelete, -1),IFNULL(files_versioning, -1) " +
"FROM ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}"
)
execSQL("DROP TABLE ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
execSQL("ALTER TABLE ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}2 RENAME TO" +
" ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
}
}
}
@@ -0,0 +1,29 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val MIGRATION_30_31 = object : Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) {
//Nothing to migrate at the moment
}
}
@@ -0,0 +1,30 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val MIGRATION_31_32 = object : Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `user_quotas` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL," +
" `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))")
}
}
@@ -0,0 +1,75 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteException
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME
import timber.log.Timber
val MIGRATION_32_33 = object : Migration(32, 33) {
override fun migrate(database: SupportSQLiteDatabase) {
try {
// 1. Create new OCShares table
database.execSQL("CREATE TABLE IF NOT EXISTS `${OCSHARES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" `share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date`" +
" INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT," +
" `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share`" +
" TEXT NOT NULL, `name` TEXT, `url` TEXT)")
// 2. Get old OCShares and insert them into the new table
val cursor = database.query("SELECT * FROM $OCSHARES_TABLE_NAME")
cursor.use {
while (it.moveToNext()) {
val cv = ContentValues()
cv.put("id", it.getInt(it.getColumnIndexOrThrow("id")))
cv.put("share_type", it.getInt(it.getColumnIndexOrThrow("share_type")))
cv.put("share_with", it.getString(it.getColumnIndexOrThrow("shate_with")))
cv.put("path", it.getString(it.getColumnIndexOrThrow("path")))
cv.put("permissions", it.getInt(it.getColumnIndexOrThrow("permissions")))
cv.put("shared_date", it.getInt(it.getColumnIndexOrThrow("shared_date")))
cv.put("expiration_date", it.getInt(it.getColumnIndexOrThrow("expiration_date")))
cv.put("token", it.getString(it.getColumnIndexOrThrow("token")))
cv.put("shared_with_display_name", it.getString(it.getColumnIndexOrThrow("shared_with_display_name")))
cv.put("share_with_additional_info", it.getString(it.getColumnIndexOrThrow("share_with_additional_info")))
cv.put("is_directory", it.getInt(it.getColumnIndexOrThrow("is_directory")))
cv.put("id_remote_shared", it.getString(it.getColumnIndexOrThrow("id_remote_shared")))
cv.put("owner_share", it.getString(it.getColumnIndexOrThrow("owner_share")))
cv.put("name", it.getString(it.getColumnIndexOrThrow("name")))
cv.put("url", it.getString(it.getColumnIndexOrThrow("url")))
database.insert("${OCSHARES_TABLE_NAME}2", 0, cv)
}
}
// 3. Drop old table and rename new one.
database.execSQL("DROP TABLE $OCSHARES_TABLE_NAME")
database.execSQL("ALTER TABLE ${OCSHARES_TABLE_NAME}2 RENAME TO $OCSHARES_TABLE_NAME")
} catch (e: SQLiteException) {
Timber.e(e, "SQLiteException in migrate from database version 32 to version 33")
} catch (e: Exception) {
Timber.e(e, "Failed to migrate database version 32 to version 33")
}
}
}
@@ -0,0 +1,112 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
@file:Suppress("MatchingDeclarationName")
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME
import eu.qsfera.android.data.providers.SharedPreferencesProvider
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.videoUploadsName
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
import java.io.File
val MIGRATION_33_34 = object : Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `$FOLDER_BACKUP_TABLE_NAME` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL," +
" `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `name` TEXT NOT NULL," +
" `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
}
}
@Deprecated("Legacy code. Only used to migrate old camera uploads configuration from ")
class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferencesProvider) {
fun getPictureUploadsConfigurationPreferences(timestamp: Long): FolderBackUpConfiguration? {
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false)) return null
return FolderBackUpConfiguration(
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, null) ?: "",
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, false),
uploadPath = getUploadPathForPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH),
sourcePath = getSourcePathForPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE),
behavior = getBehaviorForPreference(PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR),
lastSyncTimestamp = timestamp,
name = pictureUploadsName,
chargingOnly = false,
spaceId = null,
)
}
fun getVideoUploadsConfigurationPreferences(timestamp: Long): FolderBackUpConfiguration? {
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false)) return null
return FolderBackUpConfiguration(
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, null) ?: "",
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, false),
uploadPath = getUploadPathForPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH),
sourcePath = getSourcePathForPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE),
behavior = getBehaviorForPreference(PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR),
lastSyncTimestamp = timestamp,
name = videoUploadsName,
chargingOnly = false,
spaceId = null,
)
}
private fun getUploadPathForPreference(keyPreference: String): String {
val uploadPath = sharedPreferencesProvider.getString(
key = keyPreference,
defaultValue = DEFAULT_PATH_FOR_CAMERA_UPLOADS + File.separator
)
return if (uploadPath!!.endsWith(File.separator)) uploadPath else uploadPath + File.separator
}
private fun getSourcePathForPreference(keyPreference: String): String =
sharedPreferencesProvider.getString(keyPreference, null) ?: ""
private fun getBehaviorForPreference(keyPreference: String): UploadBehavior {
val storedBehaviour = sharedPreferencesProvider.getString(keyPreference, null) ?: return UploadBehavior.COPY
return UploadBehavior.fromString(storedBehaviour)
}
companion object {
private const val PREF__CAMERA_PICTURE_UPLOADS_ENABLED = "enable_picture_uploads"
private const val PREF__CAMERA_VIDEO_UPLOADS_ENABLED = "enable_video_uploads"
private const val PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "picture_uploads_on_wifi"
private const val PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "video_uploads_on_wifi"
private const val PREF__CAMERA_PICTURE_UPLOADS_PATH = "picture_uploads_path"
private const val PREF__CAMERA_VIDEO_UPLOADS_PATH = "video_uploads_path"
private const val PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR = "picture_uploads_behaviour"
private const val PREF__CAMERA_PICTURE_UPLOADS_SOURCE = "picture_uploads_source_path"
private const val PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR = "video_uploads_behaviour"
private const val PREF__CAMERA_VIDEO_UPLOADS_SOURCE = "video_uploads_source_path"
private const val PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME = "picture_uploads_account_name"
private const val PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME = "video_uploads_account_name"
private const val DEFAULT_PATH_FOR_CAMERA_UPLOADS = "/CameraUpload"
}
}
@@ -0,0 +1,33 @@
/*
* qsfera Android client application
*
* @author Fernando Sanz Velasco
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME
val MIGRATION_34_35 = object : Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
execSQL("ALTER TABLE $FOLDER_BACKUP_TABLE_NAME ADD COLUMN chargingOnly INTEGER NOT NULL DEFAULT '0'")
}
}
}
@@ -0,0 +1,33 @@
/**
* qsfera Android client application
*
* @author Fernando Sanz Velasco
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
val MIGRATION_35_36 = object : Migration(35, 36) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
execSQL("ALTER TABLE $CAPABILITIES_TABLE_NAME ADD COLUMN sharing_user_profile_picture INTEGER NOT NULL DEFAULT -1")
}
}
}
@@ -0,0 +1,40 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val MIGRATION_37_38 = object : Migration(37, 38) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `files` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `parentId` INTEGER," +
" `owner` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `remoteId` TEXT, `length` INTEGER NOT NULL, `creationTimestamp` INTEGER," +
" `modificationTimestamp` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `etag` TEXT, `permissions` TEXT, `privateLink` TEXT," +
" `storagePath` TEXT, `name` TEXT, `treeEtag` TEXT, `keepInSync` INTEGER, `lastSyncDateForData` INTEGER, `fileShareViaLink`" +
" INTEGER, `lastSyncDateForProperties` INTEGER, `needsToUpdateThumbnail` INTEGER NOT NULL, `modifiedAtLastSyncForData` INTEGER," +
" `etagInConflict` TEXT, `fileIsDownloading` INTEGER, `sharedWithSharee` INTEGER, `sharedByLink` INTEGER NOT NULL)")
database.execSQL("CREATE TABLE IF NOT EXISTS `transfers` (`localPath` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `accountName`" +
" TEXT NOT NULL, `fileSize` INTEGER NOT NULL, `status` INTEGER NOT NULL, `localBehaviour` INTEGER NOT NULL, `forceOverwrite`" +
" INTEGER NOT NULL, `transferEndTimestamp` INTEGER, `lastResult` INTEGER, `createdBy` INTEGER NOT NULL, `transferId` TEXT," +
" `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
}
}
@@ -0,0 +1,34 @@
/**
* qsfera Android client application
*
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val MIGRATION_41_42 = object : Migration(41, 42) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
execSQL("ALTER TABLE `files` ADD COLUMN `lastUsage` INTEGER")
execSQL("UPDATE `files` SET `lastUsage` = CASE WHEN `storagePath` IS NOT NULL THEN ${System.currentTimeMillis()} ELSE NULL END")
}
}
}
@@ -0,0 +1,55 @@
/**
* qsfera Android client application
*
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta
import timber.log.Timber
val MIGRATION_42_43 = object : Migration(42, 43) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
execSQL("ALTER TABLE ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME} ADD COLUMN `spaceId` TEXT")
val query = "SELECT `accountName` FROM ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME}"
val cursor = database.query(query)
cursor.use {
while (it.moveToNext()) {
val accountName = it.getString(it.getColumnIndexOrThrow("accountName"))
val spacePersonalQuery = "SELECT `space_id` FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME}\n" +
"WHERE `account_name` = '$accountName' AND `drive_type`= 'personal'"
val cursorSpacePersonal = database.query(spacePersonalQuery)
cursorSpacePersonal.use {
if (cursorSpacePersonal.moveToFirst()) {
val spaceId = cursorSpacePersonal.getString(cursorSpacePersonal.getColumnIndexOrThrow("space_id"))
execSQL("UPDATE `folder_backup` SET `spaceId` = '$spaceId' WHERE `accountName` = '$accountName'")
} else {
execSQL("UPDATE `folder_backup` SET `spaceId` = NULL WHERE `accountName` = '$accountName'")
Timber.d("No personal spaces found for account: $accountName.")
}
}
}
}
}
}
}
@@ -0,0 +1,38 @@
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
val MIGRATION_47_48 = object : Migration(47, 48) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_VERSION}` TEXT"
)
database.execSQL(
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_RESUMABLE}` TEXT"
)
database.execSQL(
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_EXTENSION}` TEXT"
)
database.execSQL(
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_MAX_CHUNK_SIZE}` INTEGER"
)
database.execSQL(
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_HTTP_METHOD_OVERRIDE}` TEXT"
)
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadUrl` TEXT")
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadLength` INTEGER")
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadMetadata` TEXT")
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadChecksum` TEXT")
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusResumableVersion` TEXT")
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadExpires` INTEGER")
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadConcat` TEXT")
}
}
@@ -0,0 +1,14 @@
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
val MIGRATION_48_49 = object : Migration(48, 49) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE ${ProviderTableMeta.FILES_TABLE_NAME} " +
"ADD COLUMN `${ProviderTableMeta.FILE_REMOTE_ETAG}` TEXT DEFAULT NULL"
)
}
}
@@ -0,0 +1,33 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.oauth.datasources
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationRequest
import eu.qsfera.android.domain.authentication.oauth.model.OIDCServerConfiguration
import eu.qsfera.android.domain.authentication.oauth.model.TokenRequest
import eu.qsfera.android.domain.authentication.oauth.model.TokenResponse
interface RemoteOAuthDataSource {
fun performOIDCDiscovery(baseUrl: String): OIDCServerConfiguration
fun performTokenRequest(tokenRequest: TokenRequest): TokenResponse
fun registerClient(clientRegistrationRequest: ClientRegistrationRequest): ClientRegistrationInfo
}
@@ -0,0 +1,154 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.oauth.datasources.implementation
import eu.qsfera.android.data.ClientManager
import eu.qsfera.android.data.executeRemoteOperation
import eu.qsfera.android.data.oauth.datasources.RemoteOAuthDataSource
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationRequest
import eu.qsfera.android.domain.authentication.oauth.model.OIDCServerConfiguration
import eu.qsfera.android.domain.authentication.oauth.model.TokenRequest
import eu.qsfera.android.domain.authentication.oauth.model.TokenResponse
import eu.qsfera.android.lib.resources.oauth.params.ClientRegistrationParams
import eu.qsfera.android.lib.resources.oauth.params.TokenRequestParams
import eu.qsfera.android.lib.resources.oauth.responses.ClientRegistrationResponse
import eu.qsfera.android.lib.resources.oauth.responses.OIDCDiscoveryResponse
import eu.qsfera.android.lib.resources.oauth.services.OIDCService
import eu.qsfera.android.lib.resources.oauth.responses.TokenResponse as RemoteTokenResponse
class OCRemoteOAuthDataSource(
private val clientManager: ClientManager,
private val oidcService: OIDCService,
) : RemoteOAuthDataSource {
override fun performOIDCDiscovery(baseUrl: String): OIDCServerConfiguration {
val qsferaClient = clientManager.getClientForAnonymousCredentials(baseUrl, false)
val serverConfiguration = executeRemoteOperation {
oidcService.getOIDCServerDiscovery(qsferaClient)
}
return serverConfiguration.toModel()
}
override fun performTokenRequest(tokenRequest: TokenRequest): TokenResponse {
// For token refreshments, a new client is required, otherwise it could keep outdated credentials or data.
val requiresNewClient = tokenRequest is TokenRequest.RefreshToken
val qsferaClient = clientManager.getClientForAnonymousCredentials(path = tokenRequest.baseUrl, requiresNewClient = requiresNewClient)
val tokenResponse = executeRemoteOperation {
oidcService.performTokenRequest(
qsferaClient = qsferaClient,
tokenRequest = tokenRequest.toParams()
)
}
return tokenResponse.toModel()
}
override fun registerClient(clientRegistrationRequest: ClientRegistrationRequest): ClientRegistrationInfo {
val qsferaClient =
clientManager.getClientForAnonymousCredentials(clientRegistrationRequest.registrationEndpoint, false)
val remoteClientRegistrationInfo = executeRemoteOperation {
oidcService.registerClientWithRegistrationEndpoint(
qsferaClient = qsferaClient,
clientRegistrationParams = clientRegistrationRequest.toParams()
)
}
return remoteClientRegistrationInfo.toModel()
}
/**************************************************************************************************************
************************************************* Mappers ****************************************************
**************************************************************************************************************/
private fun OIDCDiscoveryResponse.toModel(): OIDCServerConfiguration =
OIDCServerConfiguration(
authorizationEndpoint = this.authorizationEndpoint,
checkSessionIframe = this.checkSessionIframe,
endSessionEndpoint = this.endSessionEndpoint,
issuer = this.issuer,
registrationEndpoint = this.registrationEndpoint,
responseTypesSupported = this.responseTypesSupported,
scopesSupported = this.scopesSupported,
tokenEndpoint = this.tokenEndpoint,
tokenEndpointAuthMethodsSupported = this.tokenEndpointAuthMethodsSupported,
userInfoEndpoint = this.userinfoEndpoint
)
private fun TokenRequest.toParams(): TokenRequestParams =
when (this) {
is TokenRequest.AccessToken ->
TokenRequestParams.Authorization(
tokenEndpoint = this.tokenEndpoint,
authorizationCode = this.authorizationCode,
grantType = this.grantType,
scope = this.scope,
clientId = this.clientId,
clientSecret = this.clientSecret,
redirectUri = this.redirectUri,
clientAuth = this.clientAuth,
codeVerifier = this.codeVerifier
)
is TokenRequest.RefreshToken ->
TokenRequestParams.RefreshToken(
tokenEndpoint = this.tokenEndpoint,
grantType = this.grantType,
scope = this.scope,
clientId = this.clientId,
clientSecret = this.clientSecret,
clientAuth = this.clientAuth,
refreshToken = this.refreshToken
)
}
private fun RemoteTokenResponse.toModel(): TokenResponse =
TokenResponse(
accessToken = this.accessToken,
expiresIn = this.expiresIn,
refreshToken = this.refreshToken,
tokenType = this.tokenType,
userId = this.userId,
scope = this.scope,
idToken = this.idToken,
additionalParameters = this.additionalParameters
)
private fun ClientRegistrationRequest.toParams(): ClientRegistrationParams =
ClientRegistrationParams(
registrationEndpoint = this.registrationEndpoint,
clientName = this.clientName,
redirectUris = this.redirectUris,
tokenEndpointAuthMethod = this.tokenEndpointAuthMethod,
applicationType = this.applicationType
)
private fun ClientRegistrationResponse.toModel(): ClientRegistrationInfo =
ClientRegistrationInfo(
clientId = this.clientId,
clientSecret = this.clientSecret,
clientIdIssuedAt = this.clientIdIssuedAt,
clientSecretExpiration = this.clientSecretExpiration
)
}
@@ -0,0 +1,41 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.oauth.repository
import eu.qsfera.android.data.oauth.datasources.RemoteOAuthDataSource
import eu.qsfera.android.domain.authentication.oauth.OAuthRepository
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationRequest
import eu.qsfera.android.domain.authentication.oauth.model.OIDCServerConfiguration
import eu.qsfera.android.domain.authentication.oauth.model.TokenRequest
import eu.qsfera.android.domain.authentication.oauth.model.TokenResponse
class OCOAuthRepository(
private val oidcRemoteOAuthDataSource: RemoteOAuthDataSource,
) : OAuthRepository {
override fun performOIDCDiscovery(baseUrl: String): OIDCServerConfiguration =
oidcRemoteOAuthDataSource.performOIDCDiscovery(baseUrl)
override fun performTokenRequest(tokenRequest: TokenRequest): TokenResponse =
oidcRemoteOAuthDataSource.performTokenRequest(tokenRequest)
override fun registerClient(clientRegistrationRequest: ClientRegistrationRequest): ClientRegistrationInfo =
oidcRemoteOAuthDataSource.registerClient(clientRegistrationRequest)
}
@@ -0,0 +1,31 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
*
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.providers
import android.os.Environment
import java.io.File
@Deprecated("Do not use this anymore. We have moved to Scoped Storage")
class LegacyStorageProvider(
rootFolderName: String
) : LocalStorageProvider(rootFolderName) {
override fun getPrimaryStorageDirectory(): File = Environment.getExternalStorageDirectory()
}
@@ -0,0 +1,291 @@
/**
* qsfera Android client application
*
* @author David A. Velasco
* @author David González Verdugo
* @author Christian Schabesberger
* @author Shashvat Kedia
* @author Juan Carlos Garrote Gascón
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.data.providers
import android.accounts.Account
import android.annotation.SuppressLint
import android.net.Uri
import eu.qsfera.android.data.extensions.moveRecursively
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.transfers.model.OCTransfer
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis
sealed class LocalStorageProvider(private val rootFolderName: String) {
abstract fun getPrimaryStorageDirectory(): File
/**
* Return the root path of primary shared/external storage directory for this application.
* For example: /storage/emulated/0/qsfera
*/
fun getRootFolderPath(): String = getPrimaryStorageDirectory().absolutePath + File.separator + rootFolderName
/**
* Get local storage path for accountName.
*/
private fun getAccountDirectoryPath(
accountName: String
): String = getRootFolderPath() + File.separator + getEncodedAccountName(accountName)
/**
* Get local path where OCFile file is to be stored after upload. That is,
* corresponding local path (in local qsfera storage) to remote uploaded
* file.
*/
fun getDefaultSavePathFor(
accountName: String,
remotePath: String,
spaceId: String?,
): String =
if (spaceId != null) {
getAccountDirectoryPath(accountName) + File.separator + spaceId + File.separator + remotePath
} else {
getAccountDirectoryPath(accountName) + remotePath
}
/**
* Get expected remote path for a file creation, rename, move etc
*/
fun getExpectedRemotePath(remotePath: String, newName: String, isFolder: Boolean): String {
var parent = (File(remotePath)).parent ?: throw IllegalArgumentException("Parent path is null")
parent = if (parent.endsWith(File.separator)) parent else parent + File.separator
var newRemotePath = parent + newName
if (isFolder) {
newRemotePath += File.separator
}
return newRemotePath
}
/**
* Get absolute path to tmp folder inside datafolder in sd-card for given accountName.
*/
fun getTemporalPath(
accountName: String?,
spaceId: String? = null,
): String {
val temporalPathWithoutSpace =
getRootFolderPath() + File.separator + TEMPORAL_FOLDER_NAME + File.separator + getEncodedAccountName(accountName)
return if (spaceId != null) {
temporalPathWithoutSpace + File.separator + spaceId
} else {
temporalPathWithoutSpace
}
}
fun getLogsPath(): String = getRootFolderPath() + File.separator + LOGS_FOLDER_NAME + File.separator
/**
* Optimistic number of bytes available on sd-card.
*
* @return Optimistic number of available bytes (can be less)
*/
@SuppressLint("UsableSpace")
fun getUsableSpace(): Long = getPrimaryStorageDirectory().usableSpace
/**
* Checks if there is user data which does not have a corresponding account in the Account manager.
*/
private fun getDanglingAccountDirs(remainingAccounts: Array<Account>): List<File> {
val rootFolder = File(getRootFolderPath())
val danglingDirs = mutableListOf<File>()
rootFolder.listFiles()?.forEach { dir ->
var dirIsOk = false
if (dir.name.equals(TEMPORAL_FOLDER_NAME) || dir.name.equals(LOGS_FOLDER_NAME)) {
dirIsOk = true
} else {
remainingAccounts.forEach { account ->
if (dir.name.equals(getEncodedAccountName(account.name))) {
dirIsOk = true
}
}
}
if (!dirIsOk) {
danglingDirs.add(dir)
}
}
return danglingDirs
}
/**
* Cleans up unused files, such as deprecated user directories
*/
open fun deleteUnusedUserDirs(remainingAccounts: Array<Account>) {
val danglingDirs = getDanglingAccountDirs(remainingAccounts)
danglingDirs.forEach { dd ->
dd.deleteRecursively()
}
}
/**
* URL encoding is an 'easy fix' to overcome that NTFS and FAT32 don't allow ":" in file names,
* that can be in the accountName since 0.1.190B
*/
private fun getEncodedAccountName(accountName: String?): String = Uri.encode(accountName, "@")
fun moveLegacyToScopedStorage() {
val timeInMillis = measureTimeMillis {
moveFileOrFolderToScopedStorage(retrieveRootLegacyStorage())
}
Timber.d("MIGRATED FILES IN ${TimeUnit.SECONDS.convert(timeInMillis, TimeUnit.MILLISECONDS)} seconds")
}
private fun retrieveRootLegacyStorage(): File {
val legacyStorageProvider = LegacyStorageProvider(rootFolderName)
val rootLegacyStorage = File(legacyStorageProvider.getRootFolderPath())
val legacyStorageUsedBytes = sizeOfDirectory(rootLegacyStorage)
Timber.d(
"Root ${rootLegacyStorage.absolutePath} has ${rootLegacyStorage.listFiles()?.size} files and its size is $legacyStorageUsedBytes Bytes"
)
return rootLegacyStorage
}
private fun moveFileOrFolderToScopedStorage(rootLegacyDirectory: File) {
Timber.d("Let's move ${rootLegacyDirectory.absolutePath} to scoped storage")
rootLegacyDirectory.listFiles()?.forEach { file ->
if (file.isDirectory) {
file.moveRecursively(File(getRootFolderPath(), file.name), overwrite = true)
}
}
rootLegacyDirectory.deleteRecursively()
}
fun sizeOfDirectory(dir: File): Long {
if (dir.exists()) {
var result: Long = 0
val fileList = dir.listFiles() ?: arrayOf()
fileList.forEach { file ->
// Recursive call if it's a directory
result += if (file.isDirectory) {
sizeOfDirectory(file)
} else {
// Sum the file size in bytes
file.length()
}
}
return result // return the file size
}
return 0
}
/**
* Best-effort to remove the file locally. If storage path is null, let's try to remove it anyway.
*/
fun deleteLocalFile(ocFile: OCFile): Boolean {
val safeStoragePath = ocFile.getStoragePathOrExpectedPathForFile()
val fileToDelete = File(safeStoragePath)
if (!fileToDelete.exists()) {
return true
}
return fileToDelete.deleteRecursively()
}
fun deleteLocalFolderIfItHasNoFilesInside(ocFolder: OCFile) {
val safeStoragePath = ocFolder.getStoragePathOrExpectedPathForFile()
val folder = File(safeStoragePath)
val filesInFolder = folder.listFiles()
if (filesInFolder.isNullOrEmpty()) {
folder.delete()
}
}
fun moveLocalFile(ocFile: OCFile, finalStoragePath: String) {
val safeStoragePath = ocFile.getStoragePathOrExpectedPathForFile()
val fileToMove = File(safeStoragePath)
if (!fileToMove.exists()) {
return
}
val targetFile = File(finalStoragePath)
val targetFolder = targetFile.parentFile
if (targetFolder != null && !targetFolder.exists()) {
targetFolder.mkdirs()
}
fileToMove.renameTo(targetFile)
}
fun clearUnrelatedTemporalFiles(uploads: List<OCTransfer>, accountsNames: List<String>) {
accountsNames.forEach { accountName ->
val temporalFolderForAccount = File(getTemporalPath(accountName))
cleanTemporalRecursively(temporalFolderForAccount) { temporalFile ->
if (!uploads.map { it.localPath }.contains(temporalFile.absolutePath)) {
temporalFile.delete()
}
}
}
}
private fun cleanTemporalRecursively(temporalFolder: File, deleteFileInCaseItIsNotNeeded: (file: File) -> Unit) {
temporalFolder.listFiles()?.forEach { temporalFile ->
if (temporalFile.isDirectory) {
cleanTemporalRecursively(temporalFile, deleteFileInCaseItIsNotNeeded)
} else {
deleteFileInCaseItIsNotNeeded(temporalFile)
}
}
}
fun removeLocalStorageForAccount(accountName: String) {
val mainFolderForAccount = File(getAccountDirectoryPath(accountName))
val temporalFolderForAccount = File(getTemporalPath(accountName))
mainFolderForAccount.deleteRecursively()
temporalFolderForAccount.deleteRecursively()
}
fun deleteCacheIfNeeded(transfer: OCTransfer) {
val cacheDir = getTemporalPath(transfer.accountName)
if (transfer.localPath.startsWith(cacheDir)) {
val cacheFile = File(transfer.localPath)
cacheFile.delete()
}
}
/**
* Return the storage path if the file is already in the device storage or
* the expected storage path for the file in case it's not available locally yet.
*/
private fun OCFile.getStoragePathOrExpectedPathForFile() =
storagePath.takeUnless { it.isNullOrBlank() } ?: getDefaultSavePathFor(
accountName = owner,
remotePath = remotePath,
spaceId = spaceId,
)
companion object {
private const val LOGS_FOLDER_NAME = "logs"
private const val TEMPORAL_FOLDER_NAME = "tmp"
}
}

Some files were not shown because too many files have changed in this diff Show More