-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Philipp Heckel
committed
Mar 14, 2022
1 parent
2f0fa99
commit 8e1830d
Showing
14 changed files
with
561 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,314 @@ | ||
package io.heckel.ntfy.backup | ||
|
||
import android.content.Context | ||
import android.net.Uri | ||
import com.google.gson.Gson | ||
import com.google.gson.GsonBuilder | ||
import com.google.gson.stream.JsonReader | ||
import io.heckel.ntfy.app.Application | ||
import io.heckel.ntfy.util.Log | ||
import io.heckel.ntfy.util.topicUrl | ||
import java.io.InputStreamReader | ||
|
||
class Backuper(val context: Context) { | ||
private val gson = Gson() | ||
private val resolver = context.applicationContext.contentResolver | ||
private val repository = (context.applicationContext as Application).repository | ||
|
||
suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) { | ||
Log.d(TAG, "Backing up settings to file $uri") | ||
val json = gson.toJson(createBackupFile(withSettings, withSubscriptions, withUsers)) | ||
val outputStream = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream") | ||
outputStream.use { it.write(json.toByteArray()) } | ||
Log.d(TAG, "Backup done") | ||
} | ||
|
||
suspend fun restore(uri: Uri) { | ||
Log.d(TAG, "Restoring settings from file $uri") | ||
val reader = JsonReader(InputStreamReader(resolver.openInputStream(uri))) | ||
val backupFile = gson.fromJson<BackupFile>(reader, BackupFile::class.java) | ||
applyBackupFile(backupFile) | ||
Log.d(TAG, "Restoring done") | ||
} | ||
|
||
fun settingsAsString(): String { | ||
val gson = GsonBuilder().setPrettyPrinting().create() | ||
return gson.toJson(createSettings()) | ||
} | ||
|
||
private suspend fun applyBackupFile(backupFile: BackupFile) { | ||
if (backupFile.magic != FILE_MAGIC) { | ||
throw InvalidBackupFileException() | ||
} | ||
applySettings(backupFile.settings) | ||
applySubscriptions(backupFile.subscriptions) | ||
applyNotifications(backupFile.notifications) | ||
applyUsers(backupFile.users) | ||
} | ||
|
||
private fun applySettings(settings: Settings?) { | ||
if (settings == null) { | ||
return | ||
} | ||
if (settings.minPriority != null) { | ||
repository.setMinPriority(settings.minPriority) | ||
} | ||
if (settings.autoDownloadMaxSize != null) { | ||
repository.setAutoDownloadMaxSize(settings.autoDownloadMaxSize) | ||
} | ||
if (settings.autoDeleteSeconds != null) { | ||
repository.setAutoDeleteSeconds(settings.autoDeleteSeconds) | ||
} | ||
if (settings.darkMode != null) { | ||
repository.setDarkMode(settings.darkMode) | ||
} | ||
if (settings.connectionProtocol != null) { | ||
repository.setConnectionProtocol(settings.connectionProtocol) | ||
} | ||
if (settings.broadcastEnabled != null) { | ||
repository.setBroadcastEnabled(settings.broadcastEnabled) | ||
} | ||
if (settings.recordLogs != null) { | ||
repository.setRecordLogsEnabled(settings.recordLogs) | ||
} | ||
if (settings.defaultBaseUrl != null) { | ||
repository.setDefaultBaseUrl(settings.defaultBaseUrl) | ||
} | ||
if (settings.mutedUntil != null) { | ||
repository.setGlobalMutedUntil(settings.mutedUntil) | ||
} | ||
if (settings.lastSharedTopics != null) { | ||
settings.lastSharedTopics.forEach { repository.addLastShareTopic(it) } | ||
} | ||
} | ||
|
||
private suspend fun applySubscriptions(subscriptions: List<Subscription>?) { | ||
if (subscriptions == null) { | ||
return; | ||
} | ||
subscriptions.forEach { s -> | ||
try { | ||
repository.addSubscription(io.heckel.ntfy.db.Subscription( | ||
id = s.id, | ||
baseUrl = s.baseUrl, | ||
topic = s.topic, | ||
instant = s.instant, | ||
mutedUntil = s.mutedUntil, | ||
upAppId = s.upAppId, | ||
upConnectorToken = s.upConnectorToken | ||
)) | ||
} catch (e: Exception) { | ||
Log.w(TAG, "Unable to restore subscription ${s.id} (${topicUrl(s.baseUrl, s.topic)}): ${e.message}. Ignoring.", e) | ||
} | ||
} | ||
} | ||
|
||
private suspend fun applyNotifications(notifications: List<Notification>?) { | ||
if (notifications == null) { | ||
return; | ||
} | ||
notifications.forEach { n -> | ||
try { | ||
val attachment = if (n.attachment != null) { | ||
io.heckel.ntfy.db.Attachment( | ||
name = n.attachment.name, | ||
type = n.attachment.type, | ||
size = n.attachment.size, | ||
expires = n.attachment.expires, | ||
url = n.attachment.url, | ||
contentUri = n.attachment.contentUri, | ||
progress = n.attachment.progress, | ||
) | ||
} else { | ||
null | ||
} | ||
repository.addNotification(io.heckel.ntfy.db.Notification( | ||
id = n.id, | ||
subscriptionId = n.subscriptionId, | ||
timestamp = n.timestamp, | ||
title = n.title, | ||
message = n.message, | ||
encoding = n.encoding, | ||
notificationId = 0, | ||
priority = n.priority, | ||
tags = n.tags, | ||
click = n.click, | ||
attachment = attachment, | ||
deleted = n.deleted | ||
)) | ||
} catch (e: Exception) { | ||
Log.w(TAG, "Unable to restore notification ${n.id}: ${e.message}. Ignoring.", e) | ||
} | ||
} | ||
} | ||
|
||
private suspend fun applyUsers(users: List<User>?) { | ||
if (users == null) { | ||
return; | ||
} | ||
users.forEach { u -> | ||
try { | ||
repository.addUser(io.heckel.ntfy.db.User( | ||
baseUrl = u.baseUrl, | ||
username = u.username, | ||
password = u.password | ||
)) | ||
} catch (e: Exception) { | ||
Log.w(TAG, "Unable to restore user ${u.baseUrl} / ${u.username}: ${e.message}. Ignoring.", e) | ||
} | ||
} | ||
} | ||
|
||
private suspend fun createBackupFile(withSettings: Boolean, withSubscriptions: Boolean, withUsers: Boolean): BackupFile { | ||
return BackupFile( | ||
magic = FILE_MAGIC, | ||
version = FILE_VERSION, | ||
settings = if (withSettings) createSettings() else null, | ||
subscriptions = if (withSubscriptions) createSubscriptionList() else null, | ||
notifications = if (withSubscriptions) createNotificationList() else null, | ||
users = if (withUsers) createUserList() else null | ||
) | ||
} | ||
|
||
private fun createSettings(): Settings { | ||
return Settings( | ||
minPriority = repository.getMinPriority(), | ||
autoDownloadMaxSize = repository.getAutoDownloadMaxSize(), | ||
autoDeleteSeconds = repository.getAutoDeleteSeconds(), | ||
darkMode = repository.getDarkMode(), | ||
connectionProtocol = repository.getConnectionProtocol(), | ||
broadcastEnabled = repository.getBroadcastEnabled(), | ||
recordLogs = repository.getRecordLogs(), | ||
defaultBaseUrl = repository.getDefaultBaseUrl() ?: "", | ||
mutedUntil = repository.getGlobalMutedUntil(), | ||
lastSharedTopics = repository.getLastShareTopics() | ||
) | ||
} | ||
|
||
private suspend fun createSubscriptionList(): List<Subscription> { | ||
return repository.getSubscriptions().map { s -> | ||
Subscription( | ||
id = s.id, | ||
baseUrl = s.baseUrl, | ||
topic = s.topic, | ||
instant = s.instant, | ||
mutedUntil = s.mutedUntil, | ||
upAppId = s.upAppId, | ||
upConnectorToken = s.upConnectorToken | ||
) | ||
} | ||
} | ||
|
||
private suspend fun createNotificationList(): List<Notification> { | ||
return repository.getNotifications().map { n -> | ||
val attachment = if (n.attachment != null) { | ||
Attachment( | ||
name = n.attachment.name, | ||
type = n.attachment.type, | ||
size = n.attachment.size, | ||
expires = n.attachment.expires, | ||
url = n.attachment.url, | ||
contentUri = n.attachment.contentUri, | ||
progress = n.attachment.progress, | ||
) | ||
} else { | ||
null | ||
} | ||
Notification( | ||
id = n.id, | ||
subscriptionId = n.subscriptionId, | ||
timestamp = n.timestamp, | ||
title = n.title, | ||
message = n.message, | ||
encoding = n.encoding, | ||
priority = n.priority, | ||
tags = n.tags, | ||
click = n.click, | ||
attachment = attachment, | ||
deleted = n.deleted | ||
) | ||
} | ||
} | ||
|
||
private suspend fun createUserList(): List<User> { | ||
return repository.getUsers().map { u -> | ||
User( | ||
baseUrl = u.baseUrl, | ||
username = u.username, | ||
password = u.password | ||
) | ||
} | ||
} | ||
|
||
companion object { | ||
const val MIME_TYPE = "application/json" | ||
private const val FILE_MAGIC = "ntfy2586" | ||
private const val FILE_VERSION = 1 | ||
private const val TAG = "NtfyExporter" | ||
} | ||
} | ||
|
||
data class BackupFile( | ||
val magic: String, | ||
val version: Int, | ||
val settings: Settings?, | ||
val subscriptions: List<Subscription>?, | ||
val notifications: List<Notification>?, | ||
val users: List<User>? | ||
) | ||
|
||
data class Settings( | ||
val minPriority: Int?, | ||
val autoDownloadMaxSize: Long?, | ||
val autoDeleteSeconds: Long?, | ||
val darkMode: Int?, | ||
val connectionProtocol: String?, | ||
val broadcastEnabled: Boolean?, | ||
val recordLogs: Boolean?, | ||
val defaultBaseUrl: String?, | ||
val mutedUntil: Long?, | ||
val lastSharedTopics: List<String>?, | ||
) | ||
|
||
data class Subscription( | ||
val id: Long, | ||
val baseUrl: String, | ||
val topic: String, | ||
val instant: Boolean, | ||
val mutedUntil: Long, | ||
val upAppId: String?, | ||
val upConnectorToken: String? | ||
) | ||
|
||
data class Notification( | ||
val id: String, | ||
val subscriptionId: Long, | ||
val timestamp: Long, | ||
val title: String, | ||
val message: String, | ||
val encoding: String, // "base64" or "" | ||
val priority: Int, // 1=min, 3=default, 5=max | ||
val tags: String, | ||
val click: String, // URL/intent to open on notification click | ||
val attachment: Attachment?, | ||
val deleted: Boolean | ||
) | ||
|
||
data class Attachment( | ||
val name: String, // Filename | ||
val type: String?, // MIME type | ||
val size: Long?, // Size in bytes | ||
val expires: Long?, // Unix timestamp | ||
val url: String, // URL (mandatory, see ntfy server) | ||
val contentUri: String?, // After it's downloaded, the content:// location | ||
val progress: Int, // Progress during download, -1 if not downloaded | ||
) | ||
|
||
|
||
data class User( | ||
val baseUrl: String, | ||
val username: String, | ||
val password: String | ||
) | ||
|
||
class InvalidBackupFileException : Exception("Invalid backup file format") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.