2023-01-17 12:21:29 +01:00
|
|
|
import { ElementEntity, ListElementEntity, SomeEntity, TypeModel } from "../../common/EntityTypes.js"
|
2022-04-20 10:39:52 +02:00
|
|
|
import {
|
|
|
|
|
elementIdPart,
|
|
|
|
|
firstBiggerThanSecond,
|
|
|
|
|
GENERATED_MAX_ID,
|
|
|
|
|
GENERATED_MIN_ID,
|
|
|
|
|
getElementId,
|
|
|
|
|
listIdPart,
|
2022-12-27 15:37:40 +01:00
|
|
|
timestampToGeneratedId,
|
2022-04-20 10:39:52 +02:00
|
|
|
} from "../../common/utils/EntityUtils.js"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { CacheStorage, expandId, ExposedCacheStorage, LastUpdateTime } from "../rest/DefaultEntityRestCache.js"
|
2022-01-12 14:43:01 +01:00
|
|
|
import * as cborg from "cborg"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { EncodeOptions, Token, Type } from "cborg"
|
2023-08-16 10:47:09 +02:00
|
|
|
import {
|
|
|
|
|
assert,
|
|
|
|
|
assertNotNull,
|
|
|
|
|
DAY_IN_MILLIS,
|
|
|
|
|
getTypeId,
|
|
|
|
|
groupByAndMap,
|
|
|
|
|
groupByAndMapUniquely,
|
|
|
|
|
mapNullable,
|
|
|
|
|
splitInChunks,
|
|
|
|
|
TypeRef,
|
|
|
|
|
} from "@tutao/tutanota-utils"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { isDesktop, isOfflineStorageAvailable, isTest } from "../../common/Env.js"
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
import { modelInfos, resolveTypeReference } from "../../common/EntityFunctions.js"
|
2023-02-07 10:18:22 +01:00
|
|
|
import { AccountType, OFFLINE_STORAGE_DEFAULT_TIME_RANGE_DAYS } from "../../common/TutanotaConstants.js"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { DateProvider } from "../../common/DateProvider.js"
|
2024-01-08 17:14:09 +01:00
|
|
|
import { TokenOrNestedTokens } from "cborg/interface"
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
import {
|
|
|
|
|
CalendarEventTypeRef,
|
|
|
|
|
FileTypeRef,
|
|
|
|
|
MailBodyTypeRef,
|
|
|
|
|
MailDetailsBlobTypeRef,
|
|
|
|
|
MailDetailsDraftTypeRef,
|
|
|
|
|
MailFolderTypeRef,
|
|
|
|
|
MailHeadersTypeRef,
|
|
|
|
|
MailTypeRef,
|
|
|
|
|
} from "../../entities/tutanota/TypeRefs.js"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { UserTypeRef } from "../../entities/sys/TypeRefs.js"
|
|
|
|
|
import { OfflineStorageMigrator } from "./OfflineStorageMigrator.js"
|
|
|
|
|
import { CustomCacheHandlerMap, CustomCalendarEventCacheHandler } from "../rest/CustomCacheHandler.js"
|
|
|
|
|
import { EntityRestClient } from "../rest/EntityRestClient.js"
|
|
|
|
|
import { InterWindowEventFacadeSendDispatcher } from "../../../native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js"
|
|
|
|
|
import { SqlCipherFacade } from "../../../native/common/generatedipc/SqlCipherFacade.js"
|
2024-03-11 16:25:43 +01:00
|
|
|
import { FormattedQuery, SqlValue, TaggedSqlValue, untagSqlObject } from "./SqlValue.js"
|
2023-01-12 14:54:42 +01:00
|
|
|
import { FolderSystem } from "../../common/mail/FolderSystem.js"
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
import { isDetailsDraft, isLegacyMail } from "../../common/MailWrapper.js"
|
|
|
|
|
import { Type as TypeId } from "../../common/EntityConstants.js"
|
2023-01-04 10:54:28 +01:00
|
|
|
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
|
2023-01-12 14:56:07 +01:00
|
|
|
import { isSpamOrTrashFolder } from "../../common/mail/CommonMailUtils.js"
|
2024-03-11 16:25:43 +01:00
|
|
|
import { sql, SqlFragment } from "./Sql.js"
|
2022-01-12 14:43:01 +01:00
|
|
|
|
2023-08-16 10:47:09 +02:00
|
|
|
/**
|
|
|
|
|
* this is the value of SQLITE_MAX_VARIABLE_NUMBER in sqlite3.c
|
|
|
|
|
* it may change if the sqlite version is updated.
|
|
|
|
|
* */
|
|
|
|
|
const MAX_SAFE_SQL_VARS = 32766
|
|
|
|
|
|
2022-01-12 14:43:01 +01:00
|
|
|
function dateEncoder(data: Date, typ: string, options: EncodeOptions): TokenOrNestedTokens | null {
|
2022-10-13 13:29:00 +02:00
|
|
|
const time = data.getTime()
|
2022-01-12 14:43:01 +01:00
|
|
|
return [
|
|
|
|
|
// https://datatracker.ietf.org/doc/rfc8943/
|
|
|
|
|
new Token(Type.tag, 100),
|
2022-12-27 15:37:40 +01:00
|
|
|
new Token(time < 0 ? Type.negint : Type.uint, time),
|
2022-01-12 14:43:01 +01:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dateDecoder(bytes: number): Date {
|
|
|
|
|
return new Date(bytes)
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-27 15:37:40 +01:00
|
|
|
export const customTypeEncoders: { [typeName: string]: typeof dateEncoder } = Object.freeze({
|
|
|
|
|
Date: dateEncoder,
|
2022-01-12 14:43:01 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
type TypeDecoder = (_: any) => any
|
|
|
|
|
export const customTypeDecoders: Array<TypeDecoder> = (() => {
|
|
|
|
|
const tags: Array<TypeDecoder> = []
|
|
|
|
|
tags[100] = dateDecoder
|
|
|
|
|
return tags
|
|
|
|
|
})()
|
|
|
|
|
|
2022-10-21 15:53:39 +02:00
|
|
|
/**
|
|
|
|
|
* For each of these keys we track the current version in the database.
|
|
|
|
|
* The keys are different model versions (because we need to migrate the data with certain model version changes) and "offline" key which is used to track
|
|
|
|
|
* migrations that are needed for other reasons e.g. if DB structure changes or if we need to invalidate some tables.
|
|
|
|
|
*/
|
|
|
|
|
export type VersionMetadataBaseKey = keyof typeof modelInfos | "offline"
|
2022-04-20 10:39:52 +02:00
|
|
|
|
2022-10-21 15:53:39 +02:00
|
|
|
type VersionMetadataEntries = {
|
2022-04-20 10:39:52 +02:00
|
|
|
// Yes this is cursed, give me a break
|
2022-10-21 15:53:39 +02:00
|
|
|
[P in VersionMetadataBaseKey as `${P}-version`]: number
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
|
2022-10-21 15:53:39 +02:00
|
|
|
export interface OfflineDbMeta extends VersionMetadataEntries {
|
2022-12-27 15:37:40 +01:00
|
|
|
lastUpdateTime: number
|
2022-04-20 10:39:52 +02:00
|
|
|
timeRangeDays: number
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
const TableDefinitions = Object.freeze({
|
2022-10-21 15:53:39 +02:00
|
|
|
// plus ownerGroup added in a migration
|
2022-12-27 15:37:40 +01:00
|
|
|
list_entities:
|
|
|
|
|
"type TEXT NOT NULL, listId TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, listId, elementId)",
|
2022-10-21 15:53:39 +02:00
|
|
|
// plus ownerGroup added in a migration
|
|
|
|
|
element_entities: "type TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, elementId)",
|
2022-08-11 16:38:53 +02:00
|
|
|
ranges: "type TEXT NOT NULL, listId TEXT NOT NULL, lower TEXT NOT NULL, upper TEXT NOT NULL, PRIMARY KEY (type, listId)",
|
|
|
|
|
lastUpdateBatchIdPerGroupId: "groupId TEXT NOT NULL, batchId TEXT NOT NULL, PRIMARY KEY (groupId)",
|
|
|
|
|
metadata: "key TEXT NOT NULL, value BLOB, PRIMARY KEY (key)",
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
blob_element_entities:
|
|
|
|
|
"type TEXT NOT NULL, listId TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, listId, elementId)",
|
2022-08-11 16:38:53 +02:00
|
|
|
} as const)
|
|
|
|
|
|
2022-12-27 15:37:40 +01:00
|
|
|
type Range = { lower: string; upper: string }
|
2022-08-11 16:38:53 +02:00
|
|
|
|
2022-10-21 15:53:39 +02:00
|
|
|
export interface OfflineStorageInitArgs {
|
2022-12-27 15:37:40 +01:00
|
|
|
userId: Id
|
|
|
|
|
databaseKey: Uint8Array
|
|
|
|
|
timeRangeDays: number | null
|
2022-10-21 15:53:39 +02:00
|
|
|
forceNewDatabase: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-12 17:06:57 +02:00
|
|
|
export class OfflineStorage implements CacheStorage, ExposedCacheStorage {
|
2022-07-04 14:55:17 +02:00
|
|
|
private customCacheHandler: CustomCacheHandlerMap | null = null
|
2022-10-21 15:53:39 +02:00
|
|
|
private userId: Id | null = null
|
2022-11-28 17:38:17 +01:00
|
|
|
private timeRangeDays: number | null = null
|
2022-02-10 16:32:47 +01:00
|
|
|
|
2022-01-12 14:43:01 +01:00
|
|
|
constructor(
|
2022-08-11 16:38:53 +02:00
|
|
|
private readonly sqlCipherFacade: SqlCipherFacade,
|
2022-07-20 15:28:38 +02:00
|
|
|
private readonly interWindowEventSender: InterWindowEventFacadeSendDispatcher,
|
2022-04-20 10:39:52 +02:00
|
|
|
private readonly dateProvider: DateProvider,
|
2022-05-17 17:40:44 +02:00
|
|
|
private readonly migrator: OfflineStorageMigrator,
|
2022-01-12 14:43:01 +01:00
|
|
|
) {
|
2022-02-10 16:32:47 +01:00
|
|
|
assert(isOfflineStorageAvailable() || isTest(), "Offline storage is not available.")
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-17 17:40:44 +02:00
|
|
|
/**
|
|
|
|
|
* @return {boolean} whether the database was newly created or not
|
|
|
|
|
*/
|
2022-12-27 15:37:40 +01:00
|
|
|
async init({ userId, databaseKey, timeRangeDays, forceNewDatabase }: OfflineStorageInitArgs): Promise<boolean> {
|
2022-10-21 15:53:39 +02:00
|
|
|
this.userId = userId
|
2022-11-28 17:38:17 +01:00
|
|
|
this.timeRangeDays = timeRangeDays
|
2022-07-20 15:28:38 +02:00
|
|
|
if (forceNewDatabase) {
|
2022-08-11 16:38:53 +02:00
|
|
|
if (isDesktop()) {
|
|
|
|
|
await this.interWindowEventSender.localUserDataInvalidated(userId)
|
|
|
|
|
}
|
|
|
|
|
await this.sqlCipherFacade.deleteDb(userId)
|
2022-07-20 15:28:38 +02:00
|
|
|
}
|
2022-11-28 17:38:17 +01:00
|
|
|
// We open database here, and it is closed in the native side when the window is closed or the page is reloaded
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.sqlCipherFacade.openDb(userId, databaseKey)
|
|
|
|
|
await this.createTables()
|
2024-02-01 13:54:19 +01:00
|
|
|
|
2023-01-04 10:54:28 +01:00
|
|
|
try {
|
|
|
|
|
await this.migrator.migrate(this, this.sqlCipherFacade)
|
|
|
|
|
} catch (e) {
|
2023-01-04 16:54:58 +01:00
|
|
|
if (e instanceof OutOfSyncError) {
|
2023-02-02 14:17:02 +01:00
|
|
|
console.warn("Offline db is out of sync!", e)
|
2023-01-04 16:54:58 +01:00
|
|
|
await this.recreateDbFile(userId, databaseKey)
|
2023-02-02 14:17:02 +01:00
|
|
|
await this.migrator.migrate(this, this.sqlCipherFacade)
|
2023-01-04 16:54:58 +01:00
|
|
|
} else {
|
2023-01-04 10:54:28 +01:00
|
|
|
throw e
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-05-17 17:40:44 +02:00
|
|
|
// if nothing is written here, it means it's a new database
|
2024-01-22 10:42:11 +01:00
|
|
|
return (await this.getLastUpdateTime()).type === "never"
|
2023-01-04 10:54:28 +01:00
|
|
|
}
|
2022-11-28 17:38:17 +01:00
|
|
|
|
2023-01-04 10:54:28 +01:00
|
|
|
private async recreateDbFile(userId: string, databaseKey: Uint8Array): Promise<void> {
|
|
|
|
|
console.log(`recreating DB file for userId ${userId}`)
|
|
|
|
|
await this.sqlCipherFacade.closeDb()
|
|
|
|
|
await this.sqlCipherFacade.deleteDb(userId)
|
|
|
|
|
await this.sqlCipherFacade.openDb(userId, databaseKey)
|
|
|
|
|
await this.createTables()
|
2022-02-10 16:32:47 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-15 14:22:44 +02:00
|
|
|
/**
|
|
|
|
|
* currently, we close DBs from the native side (mainly on things like reload and on android's onDestroy)
|
|
|
|
|
*/
|
2022-07-20 15:28:38 +02:00
|
|
|
async deinit() {
|
2022-10-21 15:53:39 +02:00
|
|
|
this.userId = null
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.sqlCipherFacade.closeDb()
|
2022-07-20 15:28:38 +02:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async deleteIfExists(typeRef: TypeRef<SomeEntity>, listId: Id | null, elementId: Id): Promise<void> {
|
|
|
|
|
const type = getTypeId(typeRef)
|
2023-01-17 12:21:29 +01:00
|
|
|
let typeModel: TypeModel
|
|
|
|
|
try {
|
|
|
|
|
typeModel = await resolveTypeReference(typeRef)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// prevent failed lookup for BlobToFileMapping - this catch block can be removed after May 2023
|
|
|
|
|
console.log("couldn't resolve typeRef ", typeRef)
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-08-15 16:43:50 +02:00
|
|
|
let formattedQuery
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
switch (typeModel.type) {
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.Element:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`DELETE FROM element_entities WHERE type = ${type} AND elementId = ${elementId}`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.ListElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`DELETE FROM list_entities WHERE type = ${type} AND listId = ${listId} AND elementId = ${elementId}`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.BlobElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`DELETE FROM blob_element_entities WHERE type = ${type} AND listId = ${listId} AND elementId = ${elementId}`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("must be a persistent type")
|
2022-02-10 16:32:47 +01:00
|
|
|
}
|
2023-08-15 16:43:50 +02:00
|
|
|
await this.sqlCipherFacade.run(formattedQuery.query, formattedQuery.params)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2023-02-02 17:21:34 +01:00
|
|
|
async deleteAllOfType(typeRef: TypeRef<SomeEntity>): Promise<void> {
|
|
|
|
|
const type = getTypeId(typeRef)
|
|
|
|
|
let typeModel: TypeModel
|
|
|
|
|
try {
|
|
|
|
|
typeModel = await resolveTypeReference(typeRef)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// prevent failed lookup for BlobToFileMapping - this catch block can be removed after May 2023
|
|
|
|
|
console.log("couldn't resolve typeRef ", typeRef)
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-08-15 16:43:50 +02:00
|
|
|
let formattedQuery
|
2023-02-02 17:21:34 +01:00
|
|
|
switch (typeModel.type) {
|
|
|
|
|
case TypeId.Element:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`DELETE FROM element_entities WHERE type = ${type}`
|
2023-02-02 17:21:34 +01:00
|
|
|
break
|
|
|
|
|
case TypeId.ListElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`DELETE FROM list_entities WHERE type = ${type}`
|
|
|
|
|
await this.sqlCipherFacade.run(formattedQuery.query, formattedQuery.params)
|
2023-03-29 14:46:04 +02:00
|
|
|
await this.deleteAllRangesForType(type)
|
|
|
|
|
return
|
2023-02-02 17:21:34 +01:00
|
|
|
case TypeId.BlobElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`DELETE FROM blob_element_entities WHERE type = ${type}`
|
2023-02-02 17:21:34 +01:00
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("must be a persistent type")
|
|
|
|
|
}
|
2023-08-15 16:43:50 +02:00
|
|
|
await this.sqlCipherFacade.run(formattedQuery.query, formattedQuery.params)
|
2023-02-02 17:21:34 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-29 14:46:04 +02:00
|
|
|
private async deleteAllRangesForType(type: string): Promise<void> {
|
|
|
|
|
const { query, params } = sql`DELETE FROM ranges WHERE type = ${type}`
|
|
|
|
|
await this.sqlCipherFacade.run(query, params)
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async get<T extends SomeEntity>(typeRef: TypeRef<T>, listId: Id | null, elementId: Id): Promise<T | null> {
|
|
|
|
|
const type = getTypeId(typeRef)
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
const typeModel = await resolveTypeReference(typeRef)
|
2023-08-15 16:43:50 +02:00
|
|
|
let formattedQuery
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
switch (typeModel.type) {
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.Element:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`SELECT entity from element_entities WHERE type = ${type} AND elementId = ${elementId}`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.ListElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`SELECT entity from list_entities WHERE type = ${type} AND listId = ${listId} AND elementId = ${elementId}`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.BlobElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`SELECT entity from blob_element_entities WHERE type = ${type} AND listId = ${listId} AND elementId = ${elementId}`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("must be a persistent type")
|
2022-08-11 16:38:53 +02:00
|
|
|
}
|
2023-08-15 16:43:50 +02:00
|
|
|
const result = await this.sqlCipherFacade.get(formattedQuery.query, formattedQuery.params)
|
2022-12-27 15:37:40 +01:00
|
|
|
return result?.entity ? this.deserialize(typeRef, result.entity.value as Uint8Array) : null
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getIdsInRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<Id>> {
|
2022-08-11 16:38:53 +02:00
|
|
|
const type = getTypeId(typeRef)
|
|
|
|
|
const range = await this.getRange(type, listId)
|
|
|
|
|
if (range == null) {
|
|
|
|
|
throw new Error(`no range exists for ${type} and list ${listId}`)
|
|
|
|
|
}
|
2022-12-27 15:37:40 +01:00
|
|
|
const { lower, upper } = range
|
|
|
|
|
const { query, params } = sql`SELECT elementId FROM list_entities
|
2022-08-11 16:38:53 +02:00
|
|
|
WHERE type = ${type}
|
|
|
|
|
AND listId = ${listId}
|
|
|
|
|
AND (elementId = ${lower}
|
|
|
|
|
OR ${firstIdBigger("elementId", lower)})
|
|
|
|
|
AND NOT(${firstIdBigger("elementId", upper)})`
|
|
|
|
|
const rows = await this.sqlCipherFacade.all(query, params)
|
|
|
|
|
return rows.map((row) => row.elementId.value as string)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async getRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Range | null> {
|
|
|
|
|
return this.getRange(getTypeId(typeRef), listId)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async isElementIdInCacheRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, id: Id): Promise<boolean> {
|
|
|
|
|
const range = await this.getRangeForList(typeRef, listId)
|
2022-12-27 15:37:40 +01:00
|
|
|
return range != null && !firstBiggerThanSecond(id, range.upper) && !firstBiggerThanSecond(range.lower, id)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async provideFromRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, start: Id, count: number, reverse: boolean): Promise<T[]> {
|
2022-08-11 16:38:53 +02:00
|
|
|
const type = getTypeId(typeRef)
|
2023-08-15 16:43:50 +02:00
|
|
|
let formattedQuery
|
2022-08-11 16:38:53 +02:00
|
|
|
if (reverse) {
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`SELECT entity FROM list_entities WHERE type = ${type} AND listId = ${listId} AND ${firstIdBigger(
|
2022-12-27 15:37:40 +01:00
|
|
|
start,
|
|
|
|
|
"elementId",
|
|
|
|
|
)} ORDER BY LENGTH(elementId) DESC, elementId DESC LIMIT ${count}`
|
2022-08-11 16:38:53 +02:00
|
|
|
} else {
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`SELECT entity FROM list_entities WHERE type = ${type} AND listId = ${listId} AND ${firstIdBigger(
|
2022-12-27 15:37:40 +01:00
|
|
|
"elementId",
|
|
|
|
|
start,
|
|
|
|
|
)} ORDER BY LENGTH(elementId) ASC, elementId ASC LIMIT ${count}`
|
2022-08-11 16:38:53 +02:00
|
|
|
}
|
2023-08-15 16:43:50 +02:00
|
|
|
const { query, params } = formattedQuery
|
2022-08-11 16:38:53 +02:00
|
|
|
const serializedList: ReadonlyArray<Record<string, TaggedSqlValue>> = await this.sqlCipherFacade.all(query, params)
|
2022-12-27 15:37:40 +01:00
|
|
|
return this.deserializeList(
|
|
|
|
|
typeRef,
|
|
|
|
|
serializedList.map((r) => r.entity.value as Uint8Array),
|
|
|
|
|
)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async put(originalEntity: SomeEntity): Promise<void> {
|
|
|
|
|
const serializedEntity = this.serialize(originalEntity)
|
2022-12-27 15:37:40 +01:00
|
|
|
const { listId, elementId } = expandId(originalEntity._id)
|
2022-08-11 16:38:53 +02:00
|
|
|
const type = getTypeId(originalEntity._type)
|
2022-10-21 15:53:39 +02:00
|
|
|
const ownerGroup = originalEntity._ownerGroup
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
const typeModel = await resolveTypeReference(originalEntity._type)
|
2023-08-15 16:43:50 +02:00
|
|
|
let formattedQuery: FormattedQuery
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
switch (typeModel.type) {
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.Element:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`INSERT OR REPLACE INTO element_entities (type, elementId, ownerGroup, entity) VALUES (${type}, ${elementId}, ${ownerGroup}, ${serializedEntity})`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.ListElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`INSERT OR REPLACE INTO list_entities (type, listId, elementId, ownerGroup, entity) VALUES (${type}, ${listId}, ${elementId}, ${ownerGroup}, ${serializedEntity})`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.BlobElement:
|
2023-08-15 16:43:50 +02:00
|
|
|
formattedQuery = sql`INSERT OR REPLACE INTO blob_element_entities (type, listId, elementId, ownerGroup, entity) VALUES (${type}, ${listId}, ${elementId}, ${ownerGroup}, ${serializedEntity})`
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("must be a persistent type")
|
|
|
|
|
}
|
2023-08-15 16:43:50 +02:00
|
|
|
await this.sqlCipherFacade.run(formattedQuery.query, formattedQuery.params)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setLowerRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, id: Id): Promise<void> {
|
2022-08-11 16:38:53 +02:00
|
|
|
const type = getTypeId(typeRef)
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`UPDATE ranges SET lower = ${id} WHERE type = ${type} AND listId = ${listId}`
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.sqlCipherFacade.run(query, params)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setUpperRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, id: Id): Promise<void> {
|
2022-08-11 16:38:53 +02:00
|
|
|
const type = getTypeId(typeRef)
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`UPDATE ranges SET upper = ${id} WHERE type = ${type} AND listId = ${listId}`
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.sqlCipherFacade.run(query, params)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setNewRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lower: Id, upper: Id): Promise<void> {
|
2022-08-11 16:38:53 +02:00
|
|
|
const type = getTypeId(typeRef)
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`INSERT OR REPLACE INTO ranges VALUES (${type}, ${listId}, ${lower}, ${upper})`
|
2022-08-11 16:38:53 +02:00
|
|
|
return this.sqlCipherFacade.run(query, params)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async getLastBatchIdForGroup(groupId: Id): Promise<Id | null> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`SELECT batchId from lastUpdateBatchIdPerGroupId WHERE groupId = ${groupId}`
|
|
|
|
|
const row = (await this.sqlCipherFacade.get(query, params)) as { batchId: TaggedSqlValue } | null
|
2022-08-11 16:38:53 +02:00
|
|
|
return (row?.batchId?.value ?? null) as Id | null
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async putLastBatchIdForGroup(groupId: Id, batchId: Id): Promise<void> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`INSERT OR REPLACE INTO lastUpdateBatchIdPerGroupId VALUES (${groupId}, ${batchId})`
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.sqlCipherFacade.run(query, params)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-09-07 17:09:25 +02:00
|
|
|
async getLastUpdateTime(): Promise<LastUpdateTime> {
|
|
|
|
|
const time = await this.getMetadata("lastUpdateTime")
|
2022-12-27 15:37:40 +01:00
|
|
|
return time ? { type: "recorded", time } : { type: "never" }
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async putLastUpdateTime(ms: number): Promise<void> {
|
|
|
|
|
await this.putMetadata("lastUpdateTime", ms)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async purgeStorage(): Promise<void> {
|
|
|
|
|
for (let name of Object.keys(TableDefinitions)) {
|
|
|
|
|
await this.sqlCipherFacade.run(`DELETE FROM ${name}`, [])
|
|
|
|
|
}
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async deleteRange(typeRef: TypeRef<unknown>, listId: string): Promise<void> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`DELETE FROM ranges WHERE type = ${getTypeId(typeRef)} AND listId = ${listId}`
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.sqlCipherFacade.run(query, params)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async getListElementsOfType<T extends ListElementEntity>(typeRef: TypeRef<T>): Promise<Array<T>> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`SELECT entity from list_entities WHERE type = ${getTypeId(typeRef)}`
|
|
|
|
|
const items = (await this.sqlCipherFacade.all(query, params)) ?? []
|
|
|
|
|
return this.deserializeList(
|
|
|
|
|
typeRef,
|
|
|
|
|
items.map((row) => row.entity.value as Uint8Array),
|
|
|
|
|
)
|
2022-02-10 16:32:47 +01:00
|
|
|
}
|
|
|
|
|
|
2022-06-16 17:23:48 +02:00
|
|
|
async getElementsOfType<T extends ElementEntity>(typeRef: TypeRef<T>): Promise<Array<T>> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`SELECT entity from element_entities WHERE type = ${getTypeId(typeRef)}`
|
|
|
|
|
const items = (await this.sqlCipherFacade.all(query, params)) ?? []
|
|
|
|
|
return this.deserializeList(
|
|
|
|
|
typeRef,
|
|
|
|
|
items.map((row) => row.entity.value as Uint8Array),
|
|
|
|
|
)
|
2022-06-16 17:23:48 +02:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async getWholeList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<T>> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`SELECT entity FROM list_entities WHERE type = ${getTypeId(typeRef)} AND listId = ${listId}`
|
|
|
|
|
const items = (await this.sqlCipherFacade.all(query, params)) ?? []
|
|
|
|
|
return this.deserializeList(
|
|
|
|
|
typeRef,
|
|
|
|
|
items.map((row) => row.entity.value as Uint8Array),
|
|
|
|
|
)
|
2022-02-10 16:32:47 +01:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
async dumpMetadata(): Promise<Partial<OfflineDbMeta>> {
|
|
|
|
|
const query = "SELECT * from metadata"
|
2022-12-27 15:37:40 +01:00
|
|
|
const stored = (await this.sqlCipherFacade.all(query, [])).map((row) => [row.key.value as string, row.value.value as Uint8Array] as const)
|
2022-08-11 16:38:53 +02:00
|
|
|
return Object.fromEntries(stored.map(([key, value]) => [key, cborg.decode(value)])) as OfflineDbMeta
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
2022-10-21 15:53:39 +02:00
|
|
|
async setStoredModelVersion(model: VersionMetadataBaseKey, version: number) {
|
2022-08-11 16:38:53 +02:00
|
|
|
return this.putMetadata(`${model}-version`, version)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCustomCacheHandlerMap(entityRestClient: EntityRestClient): CustomCacheHandlerMap {
|
|
|
|
|
if (this.customCacheHandler == null) {
|
2022-12-27 15:37:40 +01:00
|
|
|
this.customCacheHandler = new CustomCacheHandlerMap({ ref: CalendarEventTypeRef, handler: new CustomCalendarEventCacheHandler(entityRestClient) })
|
2022-08-11 16:38:53 +02:00
|
|
|
}
|
|
|
|
|
return this.customCacheHandler
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-21 15:53:39 +02:00
|
|
|
getUserId(): Id {
|
|
|
|
|
return assertNotNull(this.userId, "No user id, not initialized?")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteAllOwnedBy(owner: Id): Promise<void> {
|
|
|
|
|
{
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`DELETE FROM element_entities WHERE ownerGroup = ${owner}`
|
2022-10-21 15:53:39 +02:00
|
|
|
await this.sqlCipherFacade.run(query, params)
|
|
|
|
|
}
|
|
|
|
|
{
|
2022-11-02 11:37:37 +01:00
|
|
|
// first, check which list Ids contain entities owned by the lost group
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`SELECT listId, type FROM list_entities WHERE ownerGroup = ${owner}`
|
2022-11-02 11:37:37 +01:00
|
|
|
const rangeRows = await this.sqlCipherFacade.all(query, params)
|
2022-12-27 15:37:40 +01:00
|
|
|
const rows = rangeRows.map((row) => untagSqlObject(row) as { listId: string; type: string })
|
|
|
|
|
const listIdsByType: Map<string, Set<Id>> = groupByAndMapUniquely(
|
|
|
|
|
rows,
|
|
|
|
|
(row) => row.type,
|
|
|
|
|
(row) => row.listId,
|
|
|
|
|
)
|
2023-08-16 10:47:09 +02:00
|
|
|
// delete the ranges for those listIds
|
2022-11-02 11:37:37 +01:00
|
|
|
for (const [type, listIds] of listIdsByType.entries()) {
|
2023-08-16 10:47:09 +02:00
|
|
|
// this particular query uses one other SQL var for the type.
|
|
|
|
|
const safeChunkSize = MAX_SAFE_SQL_VARS - 1
|
|
|
|
|
const listIdArr = Array.from(listIds)
|
|
|
|
|
await this.runChunked(safeChunkSize, listIdArr, (c) => sql`DELETE FROM ranges WHERE type = ${type} AND listId IN ${paramList(c)}`)
|
|
|
|
|
await this.runChunked(safeChunkSize, listIdArr, (c) => sql`DELETE FROM list_entities WHERE type = ${type} AND listId IN ${paramList(c)}`)
|
2022-11-02 11:37:37 +01:00
|
|
|
}
|
2022-11-03 11:04:26 +01:00
|
|
|
}
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
{
|
|
|
|
|
const { query, params } = sql`DELETE FROM blob_element_entities WHERE ownerGroup = ${owner}`
|
|
|
|
|
await this.sqlCipherFacade.run(query, params)
|
|
|
|
|
}
|
2022-11-03 11:04:26 +01:00
|
|
|
{
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`DELETE FROM lastUpdateBatchIdPerGroupId WHERE groupId = ${owner}`
|
2022-11-03 11:04:26 +01:00
|
|
|
await this.sqlCipherFacade.run(query, params)
|
2022-10-21 15:53:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
private async putMetadata<K extends keyof OfflineDbMeta>(key: K, value: OfflineDbMeta[K]): Promise<void> {
|
2023-02-07 10:18:22 +01:00
|
|
|
let encodedValue
|
|
|
|
|
try {
|
|
|
|
|
encodedValue = cborg.encode(value)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log("[OfflineStorage] failed to encode metadata for key", key, "with value", value)
|
|
|
|
|
throw e
|
|
|
|
|
}
|
|
|
|
|
const { query, params } = sql`INSERT OR REPLACE INTO metadata VALUES (${key}, ${encodedValue})`
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.sqlCipherFacade.run(query, params)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getMetadata<K extends keyof OfflineDbMeta>(key: K): Promise<OfflineDbMeta[K] | null> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`SELECT value from metadata WHERE key = ${key}`
|
2022-08-11 16:38:53 +02:00
|
|
|
const encoded = await this.sqlCipherFacade.get(query, params)
|
|
|
|
|
return encoded && cborg.decode(encoded.value.value as Uint8Array)
|
2022-01-12 14:43:01 +01:00
|
|
|
}
|
2022-02-10 16:32:47 +01:00
|
|
|
|
2022-05-06 14:39:31 +02:00
|
|
|
/**
|
2022-11-28 17:38:17 +01:00
|
|
|
* Clear out unneeded data from the offline database (i.e. trash and spam lists, old data).
|
|
|
|
|
* This will be called after login (CachePostLoginActions.ts) to ensure fast login time.
|
|
|
|
|
* @param timeRangeDays: the maximum age of days that mails should be to be kept in the database. if null, will use a default value
|
|
|
|
|
* @param userId id of the current user. default, last stored userId
|
2022-05-06 14:39:31 +02:00
|
|
|
*/
|
2022-11-28 17:38:17 +01:00
|
|
|
async clearExcludedData(timeRangeDays: number | null = this.timeRangeDays, userId: Id = this.getUserId()): Promise<void> {
|
2022-08-11 16:38:53 +02:00
|
|
|
const user = await this.get(UserTypeRef, null, userId)
|
2022-04-12 14:58:52 +02:00
|
|
|
|
2022-05-06 14:39:31 +02:00
|
|
|
// Free users always have default time range regardless of what is stored
|
|
|
|
|
const isFreeUser = user?.accountType === AccountType.FREE
|
|
|
|
|
const timeRange = isFreeUser || timeRangeDays == null ? OFFLINE_STORAGE_DEFAULT_TIME_RANGE_DAYS : timeRangeDays
|
2024-04-25 11:05:18 +02:00
|
|
|
const now = this.dateProvider.now()
|
|
|
|
|
const daysSinceDayAfterEpoch = now / DAY_IN_MILLIS - 1
|
|
|
|
|
const timeRangeMillisSafe = Math.min(daysSinceDayAfterEpoch, timeRange) * DAY_IN_MILLIS
|
|
|
|
|
// from may 15th 2109 onward, exceeding daysSinceDayAfterEpoch in the time range setting will
|
|
|
|
|
// lead to an overflow in our 42 bit timestamp in the id.
|
|
|
|
|
const cutoffTimestamp = now - timeRangeMillisSafe
|
2022-04-20 10:39:52 +02:00
|
|
|
const cutoffId = timestampToGeneratedId(cutoffTimestamp)
|
2023-01-12 14:54:42 +01:00
|
|
|
const folders = await this.getListElementsOfType(MailFolderTypeRef)
|
|
|
|
|
const folderSystem = new FolderSystem(folders)
|
|
|
|
|
|
|
|
|
|
for (const folder of folders) {
|
2023-01-12 14:56:07 +01:00
|
|
|
if (isSpamOrTrashFolder(folderSystem, folder)) {
|
2022-04-20 10:39:52 +02:00
|
|
|
await this.deleteMailList(folder.mails, GENERATED_MAX_ID)
|
|
|
|
|
} else {
|
|
|
|
|
await this.deleteMailList(folder.mails, cutoffId)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-11 16:38:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async createTables() {
|
|
|
|
|
for (let [name, definition] of Object.entries(TableDefinitions)) {
|
|
|
|
|
await this.sqlCipherFacade.run(`CREATE TABLE IF NOT EXISTS ${name} (${definition})`, [])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getRange(type: string, listId: Id): Promise<Range | null> {
|
2022-12-27 15:37:40 +01:00
|
|
|
const { query, params } = sql`SELECT upper, lower FROM ranges WHERE type = ${type} AND listId = ${listId}`
|
|
|
|
|
const row = (await this.sqlCipherFacade.get(query, params)) ?? null
|
2022-08-11 16:38:53 +02:00
|
|
|
return mapNullable(row, untagSqlObject) as Range | null
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This method deletes mails from {@param listId} what are older than {@param cutoffId}. as well as associated data
|
|
|
|
|
*
|
|
|
|
|
* For each mail we delete its body, headers, and all referenced attachments.
|
|
|
|
|
*
|
|
|
|
|
* When we delete the Files, we also delete the whole range for the user's File list. We need to delete the whole
|
|
|
|
|
* range because we only have one file list per mailbox, so if we delete something from the middle of it, the range
|
|
|
|
|
* will no longer be valid. (this is future proofing, because as of now there is not going to be a Range set for the
|
|
|
|
|
* File list anyway, since we currently do not do range requests for Files.
|
|
|
|
|
*
|
|
|
|
|
* We do not delete ConversationEntries because:
|
|
|
|
|
* 1. They are in the same list for the whole conversation so we can't adjust the range
|
|
|
|
|
* 2. We might need them in the future for showing the whole thread
|
|
|
|
|
*/
|
|
|
|
|
private async deleteMailList(listId: Id, cutoffId: Id): Promise<void> {
|
2022-11-30 17:15:08 +01:00
|
|
|
// We lock access to the "ranges" db here in order to prevent race conditions when accessing the "ranges" database.
|
|
|
|
|
await this.lockRangesDbAccess(listId)
|
2022-12-12 17:51:21 +01:00
|
|
|
try {
|
|
|
|
|
// This must be done before deleting mails to know what the new range has to be
|
|
|
|
|
await this.updateRangeForList(MailTypeRef, listId, cutoffId)
|
|
|
|
|
} finally {
|
|
|
|
|
// We unlock access to the "ranges" db here. We lock it in order to prevent race conditions when accessing the "ranges" database.
|
|
|
|
|
await this.unlockRangesDbAccess(listId)
|
|
|
|
|
}
|
2022-04-20 10:39:52 +02:00
|
|
|
|
|
|
|
|
const mailsToDelete: IdTuple[] = []
|
|
|
|
|
const headersToDelete: Id[] = []
|
|
|
|
|
const attachmentsTodelete: IdTuple[] = []
|
|
|
|
|
const mailbodiesToDelete: Id[] = []
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
const mailDetailsBlobToDelete: IdTuple[] = []
|
|
|
|
|
const mailDetailsDraftToDelete: IdTuple[] = []
|
2022-04-20 10:39:52 +02:00
|
|
|
|
|
|
|
|
const mails = await this.getWholeList(MailTypeRef, listId)
|
|
|
|
|
for (let mail of mails) {
|
|
|
|
|
if (firstBiggerThanSecond(cutoffId, getElementId(mail))) {
|
|
|
|
|
mailsToDelete.push(mail._id)
|
|
|
|
|
for (const id of mail.attachments) {
|
|
|
|
|
attachmentsTodelete.push(id)
|
|
|
|
|
}
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
if (isLegacyMail(mail)) {
|
|
|
|
|
mailbodiesToDelete.push(assertNotNull(mail.body))
|
|
|
|
|
} else if (isDetailsDraft(mail)) {
|
|
|
|
|
const mailDetailsId = assertNotNull(mail.mailDetailsDraft)
|
|
|
|
|
mailDetailsDraftToDelete.push(mailDetailsId)
|
|
|
|
|
} else {
|
|
|
|
|
// mailDetailsBlob
|
|
|
|
|
const mailDetailsId = assertNotNull(mail.mailDetails)
|
|
|
|
|
mailDetailsBlobToDelete.push(mailDetailsId)
|
|
|
|
|
}
|
|
|
|
|
if (mail.headers) {
|
|
|
|
|
headersToDelete.push(mail.headers)
|
|
|
|
|
}
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.deleteIn(MailBodyTypeRef, null, mailbodiesToDelete)
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
for (let [listId, elementIds] of groupByAndMap(mailDetailsBlobToDelete, listIdPart, elementIdPart).entries()) {
|
|
|
|
|
await this.deleteIn(MailDetailsBlobTypeRef, listId, elementIds)
|
|
|
|
|
}
|
|
|
|
|
for (let [listId, elementIds] of groupByAndMap(mailDetailsDraftToDelete, listIdPart, elementIdPart).entries()) {
|
|
|
|
|
await this.deleteIn(MailDetailsDraftTypeRef, listId, elementIds)
|
|
|
|
|
}
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.deleteIn(MailHeadersTypeRef, null, headersToDelete)
|
2022-04-20 10:39:52 +02:00
|
|
|
for (let [listId, elementIds] of groupByAndMap(attachmentsTodelete, listIdPart, elementIdPart).entries()) {
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.deleteIn(FileTypeRef, listId, elementIds)
|
|
|
|
|
await this.deleteRange(FileTypeRef, listId)
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.deleteIn(MailTypeRef, listId, mailsToDelete.map(elementIdPart))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteIn(typeRef: TypeRef<unknown>, listId: Id | null, elementIds: Id[]): Promise<void> {
|
2023-09-18 12:07:46 +02:00
|
|
|
if (elementIds.length === 0) return
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
const typeModel = await resolveTypeReference(typeRef)
|
|
|
|
|
switch (typeModel.type) {
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.Element:
|
2023-08-16 10:47:09 +02:00
|
|
|
return await this.runChunked(
|
|
|
|
|
MAX_SAFE_SQL_VARS - 1,
|
|
|
|
|
elementIds,
|
|
|
|
|
(c) => sql`DELETE FROM element_entities WHERE type = ${getTypeId(typeRef)} AND elementId IN ${paramList(c)}`,
|
|
|
|
|
)
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.ListElement:
|
2023-08-16 10:47:09 +02:00
|
|
|
return await this.runChunked(
|
|
|
|
|
MAX_SAFE_SQL_VARS - 2,
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
elementIds,
|
2023-08-16 10:47:09 +02:00
|
|
|
(c) => sql`DELETE FROM list_entities WHERE type = ${getTypeId(typeRef)} AND listId = ${listId} AND elementId IN ${paramList(c)}`,
|
|
|
|
|
)
|
2023-01-12 16:48:28 +01:00
|
|
|
case TypeId.BlobElement:
|
2023-08-16 10:47:09 +02:00
|
|
|
return await this.runChunked(
|
|
|
|
|
MAX_SAFE_SQL_VARS - 2,
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
elementIds,
|
2023-08-16 10:47:09 +02:00
|
|
|
(c) => sql`DELETE FROM blob_element_entities WHERE type = ${getTypeId(typeRef)} AND listId = ${listId} AND elementId IN ${paramList(c)}`,
|
|
|
|
|
)
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
default:
|
|
|
|
|
throw new Error("must be a persistent type")
|
|
|
|
|
}
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
|
2022-11-30 17:15:08 +01:00
|
|
|
/**
|
|
|
|
|
* We want to lock the access to the "ranges" db when updating / reading the
|
|
|
|
|
* offline available mail list ranges for each mail list (referenced using the listId).
|
|
|
|
|
* @param listId the mail list that we want to lock
|
|
|
|
|
*/
|
|
|
|
|
async lockRangesDbAccess(listId: Id) {
|
|
|
|
|
await this.sqlCipherFacade.lockRangesDbAccess(listId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This is the counterpart to the function "lockRangesDbAccess(listId)".
|
|
|
|
|
* @param listId the mail list that we want to unlock
|
|
|
|
|
*/
|
|
|
|
|
async unlockRangesDbAccess(listId: Id) {
|
|
|
|
|
await this.sqlCipherFacade.unlockRangesDbAccess(listId)
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-20 10:39:52 +02:00
|
|
|
private async updateRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, cutoffId: Id): Promise<void> {
|
2022-05-23 17:09:09 +02:00
|
|
|
const type = getTypeId(typeRef)
|
2022-04-20 10:39:52 +02:00
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
const range = await this.getRange(type, listId)
|
2022-04-20 10:39:52 +02:00
|
|
|
if (range == null) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the range for a given list is complete from the beginning (starts at GENERATED_MIN_ID), then we only want to actually modify the
|
|
|
|
|
// saved range if we would be removing elements from the list, in order to not lose the information that the range is complete in storage.
|
|
|
|
|
// So we have to check how old the oldest element in said range is. If it is newer than cutoffId, then we will not modify the range,
|
|
|
|
|
// otherwise we will just modify it normally
|
|
|
|
|
if (range.lower === GENERATED_MIN_ID) {
|
|
|
|
|
const entities = await this.provideFromRange(typeRef, listId, GENERATED_MIN_ID, 1, false)
|
|
|
|
|
const id = mapNullable(entities[0], getElementId)
|
|
|
|
|
const rangeWontBeModified = id == null || firstBiggerThanSecond(id, cutoffId) || id === cutoffId
|
|
|
|
|
if (rangeWontBeModified) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (firstBiggerThanSecond(cutoffId, range.lower)) {
|
|
|
|
|
// If the upper id of the range is below the cutoff, then the entire range will be deleted from the storage
|
|
|
|
|
// so we just delete the range as well
|
|
|
|
|
// Otherwise, we only want to modify
|
|
|
|
|
if (firstBiggerThanSecond(cutoffId, range.upper)) {
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.deleteRange(typeRef, listId)
|
2022-04-20 10:39:52 +02:00
|
|
|
} else {
|
2022-08-11 16:38:53 +02:00
|
|
|
await this.setLowerRangeForList(typeRef, listId, cutoffId)
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
private serialize(originalEntity: SomeEntity): Uint8Array {
|
2023-02-07 10:18:22 +01:00
|
|
|
try {
|
|
|
|
|
return cborg.encode(originalEntity, { typeEncoders: customTypeEncoders })
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log("[OfflineStorage] failed to encode entity of type", originalEntity._type, "with id", originalEntity._id)
|
|
|
|
|
throw e
|
|
|
|
|
}
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
|
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
private deserialize<T extends SomeEntity>(typeRef: TypeRef<T>, loaded: Uint8Array): T {
|
2022-12-27 15:37:40 +01:00
|
|
|
const deserialized = cborg.decode(loaded, { tags: customTypeDecoders })
|
2022-08-11 16:38:53 +02:00
|
|
|
// TypeRef cannot be deserialized back automatically. We could write a codec for it but we don't actually
|
|
|
|
|
// need to store it so we just "patch" it.
|
|
|
|
|
// Some places rely on TypeRef being a class and not a plain object.
|
|
|
|
|
deserialized._type = typeRef
|
|
|
|
|
return deserialized
|
2022-04-20 10:39:52 +02:00
|
|
|
}
|
2022-05-17 17:40:44 +02:00
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
private deserializeList<T extends SomeEntity>(typeRef: TypeRef<T>, loaded: Array<Uint8Array>): Array<T> {
|
2022-12-27 15:37:40 +01:00
|
|
|
return loaded.map((entity) => this.deserialize(typeRef, entity))
|
2022-05-17 17:40:44 +02:00
|
|
|
}
|
2023-08-16 10:47:09 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* convenience method to run a potentially too large query over several chunks.
|
|
|
|
|
* chunkSize must be chosen such that the total number of SQL variables in the final query does not exceed MAX_SAFE_SQL_VARS
|
|
|
|
|
* */
|
|
|
|
|
private async runChunked(chunkSize: number, originalList: SqlValue[], formatter: (chunk: SqlValue[]) => FormattedQuery): Promise<void> {
|
|
|
|
|
for (const chunk of splitInChunks(chunkSize, originalList)) {
|
|
|
|
|
const formattedQuery = formatter(chunk)
|
|
|
|
|
await this.sqlCipherFacade.run(formattedQuery.query, formattedQuery.params)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-11 16:38:53 +02:00
|
|
|
}
|
2022-07-04 14:55:17 +02:00
|
|
|
|
2022-08-11 16:38:53 +02:00
|
|
|
/*
|
2023-08-16 10:47:09 +02:00
|
|
|
* used to automatically create the right amount of SQL variables for selecting ids from a dynamic list.
|
|
|
|
|
* must be used within sql`<query>` template string to inline the logic into the query.
|
|
|
|
|
*
|
|
|
|
|
* It is very important that params is kept to a size such that the total amount of SQL variables is
|
|
|
|
|
* less than MAX_SAFE_SQL_VARS.
|
2022-08-11 16:38:53 +02:00
|
|
|
*/
|
|
|
|
|
function paramList(params: SqlValue[]): SqlFragment {
|
2022-12-27 15:37:40 +01:00
|
|
|
const qs = params.map(() => "?").join(",")
|
2022-08-11 16:38:53 +02:00
|
|
|
return new SqlFragment(`(${qs})`, params)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-08-15 14:22:44 +02:00
|
|
|
* comparison to select ids that are bigger or smaller than a parameter id
|
2023-08-16 10:47:09 +02:00
|
|
|
* must be used within sql`<query>` template string to inline the logic into the query.
|
|
|
|
|
*
|
|
|
|
|
* will always insert 3 constants and 3 SQL variables into the query.
|
2022-08-11 16:38:53 +02:00
|
|
|
*/
|
|
|
|
|
function firstIdBigger(...args: [string, "elementId"] | ["elementId", string]): SqlFragment {
|
|
|
|
|
let [l, r]: [string, string] = args
|
|
|
|
|
let v
|
|
|
|
|
if (l === "elementId") {
|
|
|
|
|
v = r
|
|
|
|
|
r = "?"
|
|
|
|
|
} else {
|
|
|
|
|
v = l
|
|
|
|
|
l = "?"
|
|
|
|
|
}
|
2022-12-27 15:37:40 +01:00
|
|
|
return new SqlFragment(`(CASE WHEN length(${l}) > length(${r}) THEN 1 WHEN length(${l}) < length(${r}) THEN 0 ELSE ${l} > ${r} END)`, [v, v, v])
|
2022-08-11 16:38:53 +02:00
|
|
|
}
|