You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	This commit is contained in:
		| @@ -1061,6 +1061,7 @@ packages/lib/themes/solarizedLight.js | ||||
| packages/lib/themes/type.js | ||||
| packages/lib/time.js | ||||
| packages/lib/types.js | ||||
| packages/lib/utils/ActionLogger.js | ||||
| packages/lib/utils/credentialFiles.js | ||||
| packages/lib/utils/joplinCloud.js | ||||
| packages/lib/utils/processStartFlags.js | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1041,6 +1041,7 @@ packages/lib/themes/solarizedLight.js | ||||
| packages/lib/themes/type.js | ||||
| packages/lib/time.js | ||||
| packages/lib/types.js | ||||
| packages/lib/utils/ActionLogger.js | ||||
| packages/lib/utils/credentialFiles.js | ||||
| packages/lib/utils/joplinCloud.js | ||||
| packages/lib/utils/processStartFlags.js | ||||
|   | ||||
| @@ -95,7 +95,7 @@ class Application extends BaseApplication { | ||||
| 			let item = null; | ||||
| 			if (type === BaseModel.TYPE_NOTE) { | ||||
| 				if (!parent) throw new Error(_('No notebook has been specified.')); | ||||
| 				item = await ItemClass.loadFolderNoteByField(parent.id, 'title', pattern); | ||||
| 				item = await (ItemClass as typeof Note).loadFolderNoteByField(parent.id, 'title', pattern); | ||||
| 			} else { | ||||
| 				item = await ItemClass.loadByTitle(pattern); | ||||
| 			} | ||||
|   | ||||
| @@ -28,7 +28,7 @@ class Command extends BaseCommand { | ||||
| 		const ok = force ? true : await this.prompt(msg, { booleanAnswerDefault: 'n' }); | ||||
| 		if (!ok) return; | ||||
|  | ||||
| 		await Folder.delete(folder.id, { toTrash: true }); | ||||
| 		await Folder.delete(folder.id, { toTrash: true, sourceDescription: 'rmbook command' }); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class Command extends BaseCommand { | ||||
| 		if (!ok) return; | ||||
|  | ||||
| 		const ids = notes.map(n => n.id); | ||||
| 		await Note.batchDelete(ids, { toTrash: true }); | ||||
| 		await Note.batchDelete(ids, { toTrash: true, sourceDescription: 'rmnote command' }); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -85,7 +85,7 @@ class Command extends BaseCommand { | ||||
|  | ||||
| 			for (let i = 0; i < noteCount; i++) { | ||||
| 				const noteId = randomElement(noteIds); | ||||
| 				promises.push(Note.delete(noteId)); | ||||
| 				promises.push(Note.delete(noteId, { sourceDescription: 'command-testing' })); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -25,7 +25,7 @@ export const runtime = (): CommandRuntime => { | ||||
| 			const ok = bridge().showConfirmMessageBox(deleteMessage); | ||||
| 			if (!ok) return; | ||||
|  | ||||
| 			await Folder.delete(folderId, { toTrash: true }); | ||||
| 			await Folder.delete(folderId, { toTrash: true, sourceDescription: 'deleteFolder command' }); | ||||
| 		}, | ||||
| 		enabledCondition: '!folderIsReadOnly', | ||||
| 	}; | ||||
|   | ||||
| @@ -13,7 +13,7 @@ export const runtime = (): CommandRuntime => { | ||||
| 		execute: async (context: CommandContext, noteIds: string[] = null) => { | ||||
| 			if (noteIds === null) noteIds = context.state.selectedNoteIds; | ||||
| 			if (!noteIds.length) return; | ||||
| 			await Note.batchDelete(noteIds, { toTrash: true }); | ||||
| 			await Note.batchDelete(noteIds, { toTrash: true, sourceDescription: 'deleteNote command' }); | ||||
|  | ||||
| 			context.dispatch({ | ||||
| 				type: 'ITEMS_TRASHED', | ||||
|   | ||||
| @@ -21,7 +21,9 @@ export const runtime = (): CommandRuntime => { | ||||
| 				defaultId: 1, | ||||
| 			}); | ||||
|  | ||||
| 			if (ok) await Note.batchDelete(noteIds, { toTrash: false }); | ||||
| 			if (ok) { | ||||
| 				await Note.batchDelete(noteIds, { toTrash: false, sourceDescription: 'permanentlyDeleteNote command' }); | ||||
| 			} | ||||
| 		}, | ||||
| 		enabledCondition: '(!noteIsReadOnly || inTrash) && someNotesSelected', | ||||
| 	}; | ||||
|   | ||||
| @@ -174,9 +174,10 @@ class ResourceScreenComponent extends React.Component<Props, State> { | ||||
| 		if (!ok) { | ||||
| 			return; | ||||
| 		} | ||||
| 		Resource.delete(resource.id) | ||||
| 		Resource.delete(resource.id, { sourceDescription: 'ResourceScreen' }) | ||||
| 		// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied | ||||
| 			.catch((error: Error) => { | ||||
| 				console.error(error); | ||||
| 				bridge().showErrorMessageBox(error.message); | ||||
| 			}) | ||||
| 		// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied | ||||
|   | ||||
| @@ -277,7 +277,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade | ||||
| 		this.props.dispatch({ type: 'NOTE_SELECTION_END' }); | ||||
|  | ||||
| 		try { | ||||
| 			await Note.batchDelete(noteIds, { toTrash: true }); | ||||
| 			await Note.batchDelete(noteIds, { toTrash: true, sourceDescription: 'Delete selected notes button' }); | ||||
| 		} catch (error) { | ||||
| 			alert(_n('This note could not be deleted: %s', 'These notes could not be deleted: %s', noteIds.length, error.message)); | ||||
| 		} | ||||
|   | ||||
| @@ -682,7 +682,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B | ||||
|  | ||||
| 		const folderId = note.parent_id; | ||||
|  | ||||
| 		await Note.delete(note.id, { toTrash: true }); | ||||
| 		await Note.delete(note.id, { toTrash: true, sourceDescription: 'Delete note button' }); | ||||
|  | ||||
| 		this.props.dispatch({ | ||||
| 			type: 'NAV_GO', | ||||
|   | ||||
| @@ -187,7 +187,7 @@ const SideMenuContentComponent = (props: Props) => { | ||||
| 						{ | ||||
| 							text: _('OK'), | ||||
| 							onPress: () => { | ||||
| 								void Folder.delete(folder.id, { toTrash: true }); | ||||
| 								void Folder.delete(folder.id, { toTrash: true, sourceDescription: 'side-menu-content (long-press)' }); | ||||
| 							}, | ||||
| 						}, | ||||
| 						{ | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import uuid from './uuid'; | ||||
| import time from './time'; | ||||
| import JoplinDatabase, { TableField } from './JoplinDatabase'; | ||||
| import { LoadOptions, SaveOptions } from './models/utils/types'; | ||||
| import ActionLogger, { ItemActionType as ItemActionType } from './utils/ActionLogger'; | ||||
| import { SqlQuery } from './services/database/types'; | ||||
| const Mutex = require('async-mutex').Mutex; | ||||
|  | ||||
| @@ -41,6 +42,9 @@ export interface DeleteOptions { | ||||
|  | ||||
| 	disableReadOnlyCheck?: boolean; | ||||
|  | ||||
| 	// Used for logging | ||||
| 	sourceDescription?: string|ActionLogger; | ||||
|  | ||||
| 	// Tells whether the deleted item should be moved to the trash. By default | ||||
| 	// it is permanently deleted. | ||||
| 	toTrash?: boolean; | ||||
| @@ -688,13 +692,17 @@ class BaseModel { | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| 	public static delete(id: string) { | ||||
| 	public static delete(id: string, options?: DeleteOptions) { | ||||
| 		if (!id) throw new Error('Cannot delete object without an ID'); | ||||
| 		ActionLogger.from(options?.sourceDescription).log(ItemActionType.Delete, id); | ||||
|  | ||||
| 		return this.db().exec(`DELETE FROM ${this.tableName()} WHERE id = ?`, [id]); | ||||
| 	} | ||||
|  | ||||
| 	public static async batchDelete(ids: string[], options: DeleteOptions = null) { | ||||
| 	public static async batchDelete(ids: string[], options?: DeleteOptions) { | ||||
| 		if (!ids.length) return; | ||||
| 		ActionLogger.from(options?.sourceDescription).log(ItemActionType.Delete, ids); | ||||
|  | ||||
| 		options = this.modOptions(options); | ||||
| 		const idFieldName = options.idFieldName ? options.idFieldName : 'id'; | ||||
| 		const sql = `DELETE FROM ${this.tableName()} WHERE ${idFieldName} IN ("${ids.join('","')}")`; | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import Resource from './models/Resource'; | ||||
| import ItemChange from './models/ItemChange'; | ||||
| import ResourceLocalState from './models/ResourceLocalState'; | ||||
| import MasterKey from './models/MasterKey'; | ||||
| import BaseModel, { ModelType } from './BaseModel'; | ||||
| import BaseModel, { DeleteOptions, ModelType } from './BaseModel'; | ||||
| import time from './time'; | ||||
| import ResourceService from './services/ResourceService'; | ||||
| import EncryptionService from './services/e2ee/EncryptionService'; | ||||
| @@ -404,7 +404,7 @@ export default class Synchronizer { | ||||
|  | ||||
| 		this.logSyncOperation('starting', null, null, `Starting synchronisation to target ${syncTargetId}... supportsAccurateTimestamp = ${this.api().supportsAccurateTimestamp}; supportsMultiPut = ${this.api().supportsMultiPut}} [${synchronizationId}]`); | ||||
|  | ||||
| 		const handleCannotSyncItem = async (ItemClass: any, syncTargetId: any, item: any, cannotSyncReason: string, itemLocation: any = null) => { | ||||
| 		const handleCannotSyncItem = async (ItemClass: typeof BaseItem, syncTargetId: any, item: any, cannotSyncReason: string, itemLocation: any = null) => { | ||||
| 			await ItemClass.saveSyncDisabled(syncTargetId, item, cannotSyncReason, itemLocation); | ||||
| 		}; | ||||
|  | ||||
| @@ -1005,7 +1005,14 @@ export default class Synchronizer { | ||||
| 							} | ||||
|  | ||||
| 							const ItemClass = BaseItem.itemClass(local.type_); | ||||
| 							await ItemClass.delete(local.id, { trackDeleted: false, changeSource: ItemChange.SOURCE_SYNC }); | ||||
| 							await ItemClass.delete( | ||||
| 								local.id, | ||||
| 								{ | ||||
| 									trackDeleted: false, | ||||
| 									changeSource: ItemChange.SOURCE_SYNC, | ||||
| 									sourceDescription: 'sync: deleteLocal', | ||||
| 								}, | ||||
| 							); | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| @@ -1050,7 +1057,14 @@ export default class Synchronizer { | ||||
| 							// CONFLICT | ||||
| 							await Folder.markNotesAsConflict(item.id); | ||||
| 						} | ||||
| 						await Folder.delete(item.id, { deleteChildren: false, changeSource: ItemChange.SOURCE_SYNC, trackDeleted: false }); | ||||
|  | ||||
| 						const deletionOptions: DeleteOptions = { | ||||
| 							deleteChildren: false, | ||||
| 							trackDeleted: false, | ||||
| 							changeSource: ItemChange.SOURCE_SYNC, | ||||
| 							sourceDescription: 'Sync', | ||||
| 						}; | ||||
| 						await Folder.delete(item.id, deletionOptions); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
|   | ||||
| @@ -72,7 +72,7 @@ export default async (store: any, _next: any, action: any, dispatch: Dispatch) = | ||||
| 		for (const noteId of noteIds) { | ||||
| 			if (action.id === noteId) continue; | ||||
| 			reg.logger().info('Provisional was not modified - deleting it'); | ||||
| 			await Note.delete(noteId); | ||||
| 			await Note.delete(noteId, { sourceDescription: 'reduxSharedMiddleware: Delete provisional note' }); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -168,7 +168,7 @@ export default class BaseItem extends BaseModel { | ||||
| 		return p[0].length === 32 && p[1] === 'md'; | ||||
| 	} | ||||
|  | ||||
| 	public static itemClass(item: any): any { | ||||
| 	public static itemClass(item: any): typeof BaseItem { | ||||
| 		if (!item) throw new Error('Item cannot be null'); | ||||
|  | ||||
| 		if (typeof item === 'object') { | ||||
| @@ -269,17 +269,17 @@ export default class BaseItem extends BaseModel { | ||||
| 		return ItemClass.load(id, options); | ||||
| 	} | ||||
|  | ||||
| 	public static deleteItem(itemType: ModelType, id: string) { | ||||
| 	public static deleteItem(itemType: ModelType, id: string, options: DeleteOptions) { | ||||
| 		const ItemClass = this.itemClass(itemType); | ||||
| 		return ItemClass.delete(id); | ||||
| 		return ItemClass.delete(id, options); | ||||
| 	} | ||||
|  | ||||
| 	public static async delete(id: string, options: DeleteOptions = null) { | ||||
| 	public static async delete(id: string, options?: DeleteOptions) { | ||||
| 		return this.batchDelete([id], options); | ||||
| 	} | ||||
|  | ||||
| 	public static async batchDelete(ids: string[], options: DeleteOptions = null) { | ||||
| 		if (!options) options = {}; | ||||
| 	public static async batchDelete(ids: string[], options: DeleteOptions) { | ||||
| 		if (!options) options = { sourceDescription: '' }; | ||||
| 		let trackDeleted = true; | ||||
| 		if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted; | ||||
|  | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import Logger from '@joplin/utils/Logger'; | ||||
| import syncDebugLog from '../services/synchronizer/syncDebugLog'; | ||||
| import ResourceService from '../services/ResourceService'; | ||||
| import { LoadOptions } from './utils/types'; | ||||
| import ActionLogger from '../utils/ActionLogger'; | ||||
| import { getTrashFolder, getTrashFolderId } from '../services/trash'; | ||||
| const { substrWithEllipsis } = require('../string-utils.js'); | ||||
|  | ||||
| @@ -107,7 +108,7 @@ export default class Folder extends BaseItem { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public static async delete(folderId: string, options: DeleteOptions = null) { | ||||
| 	public static async delete(folderId: string, options?: DeleteOptions) { | ||||
| 		options = { | ||||
| 			deleteChildren: true, | ||||
| 			...options, | ||||
| @@ -120,9 +121,14 @@ export default class Folder extends BaseItem { | ||||
| 		const folder = await Folder.load(folderId); | ||||
| 		if (!folder) return; // noop | ||||
|  | ||||
| 		const actionLogger = ActionLogger.from(options.sourceDescription); | ||||
| 		actionLogger.addDescription(`folder title: ${JSON.stringify(folder.title)}`); | ||||
| 		options.sourceDescription = actionLogger; | ||||
|  | ||||
| 		if (options.deleteChildren) { | ||||
| 			const childrenDeleteOptions: DeleteOptions = { | ||||
| 				disableReadOnlyCheck: options.disableReadOnlyCheck, | ||||
| 				sourceDescription: actionLogger, | ||||
| 				deleteChildren: true, | ||||
| 				toTrash, | ||||
| 			}; | ||||
|   | ||||
| @@ -16,6 +16,7 @@ const { pregQuote, substrWithEllipsis } = require('../string-utils.js'); | ||||
| const { _ } = require('../locale'); | ||||
| import { pull, removeElement, unique } from '../ArrayUtils'; | ||||
| import { LoadOptions, SaveOptions } from './utils/types'; | ||||
| import ActionLogger from '../utils/ActionLogger'; | ||||
| import { getDisplayParentId, getTrashFolderId } from '../services/trash'; | ||||
| const urlUtils = require('../urlUtils.js'); | ||||
| const { isImageMimeType } = require('../resourceUtils'); | ||||
| @@ -827,7 +828,7 @@ export default class Note extends BaseItem { | ||||
| 		return savedNote; | ||||
| 	} | ||||
|  | ||||
| 	public static async batchDelete(ids: string[], options: DeleteOptions = null) { | ||||
| 	public static async batchDelete(ids: string[], options: DeleteOptions = {}) { | ||||
| 		if (!ids.length) return; | ||||
|  | ||||
| 		ids = ids.slice(); | ||||
| @@ -871,7 +872,12 @@ export default class Note extends BaseItem { | ||||
|  | ||||
| 				await this.db().exec({ sql, params }); | ||||
| 			} else { | ||||
| 				await super.batchDelete(processIds, options); | ||||
| 				// For now, we intentionally log only permanent batchDeletions. | ||||
| 				const actionLogger = ActionLogger.from(options.sourceDescription); | ||||
| 				const noteTitles = notes.map(note => note.title); | ||||
| 				actionLogger.addDescription(`titles: ${JSON.stringify(noteTitles)}`); | ||||
|  | ||||
| 				await super.batchDelete(processIds, { ...options, sourceDescription: actionLogger }); | ||||
| 			} | ||||
|  | ||||
| 			for (let i = 0; i < processIds.length; i++) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import BaseModel from '../BaseModel'; | ||||
| import BaseModel, { DeleteOptions } from '../BaseModel'; | ||||
| import BaseItem from './BaseItem'; | ||||
| import ItemChange from './ItemChange'; | ||||
| import NoteResource from './NoteResource'; | ||||
| @@ -22,6 +22,7 @@ import { htmlentities } from '@joplin/utils/html'; | ||||
| import { RecognizeResultLine } from '../services/ocr/utils/types'; | ||||
| import eventManager, { EventName } from '../eventManager'; | ||||
| import { unique } from '../array'; | ||||
| import ActionLogger from '../utils/ActionLogger'; | ||||
| import isSqliteSyntaxError from '../services/database/isSqliteSyntaxError'; | ||||
| import { internalUrl, isResourceUrl, isSupportedImageMimeType, resourceFilename, resourceFullPath, resourcePathToId, resourceRelativePath, resourceUrlToId } from './utils/resourceUtils'; | ||||
|  | ||||
| @@ -311,7 +312,9 @@ export default class Resource extends BaseItem { | ||||
| 		return this.db().exec('UPDATE resources set `size` = ? WHERE id = ?', [fileSize, resourceId]); | ||||
| 	} | ||||
|  | ||||
| 	public static async batchDelete(ids: string[], options: any = null) { | ||||
| 	public static async batchDelete(ids: string[], options: DeleteOptions = {}) { | ||||
| 		const actionLogger = ActionLogger.from(options.sourceDescription); | ||||
|  | ||||
| 		// For resources, there's not really batch deletion since there's the | ||||
| 		// file data to delete too, so each is processed one by one with the | ||||
| 		// file data being deleted last since the metadata deletion call may | ||||
| @@ -321,14 +324,21 @@ export default class Resource extends BaseItem { | ||||
| 			const resource = await Resource.load(id); | ||||
| 			if (!resource) continue; | ||||
|  | ||||
| 			// Log just for the current item. | ||||
| 			const logger = actionLogger.clone(); | ||||
| 			logger.addDescription(`title: ${resource.title}`); | ||||
|  | ||||
| 			const path = Resource.fullPath(resource); | ||||
| 			await super.batchDelete([id], options); | ||||
| 			await super.batchDelete([id], { | ||||
| 				...options, | ||||
| 				sourceDescription: logger, | ||||
| 			}); | ||||
| 			await this.fsDriver().remove(path); | ||||
| 			await NoteResource.deleteByResource(id); // Clean up note/resource relationships | ||||
| 			await this.db().exec('DELETE FROM items_normalized WHERE item_id = ?', [id]); | ||||
| 		} | ||||
|  | ||||
| 		await ResourceLocalState.batchDelete(ids); | ||||
| 		await ResourceLocalState.batchDelete(ids, { sourceDescription: actionLogger }); | ||||
| 	} | ||||
|  | ||||
| 	public static async markForDownload(resourceId: string) { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import BaseModel from '../BaseModel'; | ||||
| import BaseModel, { DeleteOptions } from '../BaseModel'; | ||||
| import { ResourceLocalStateEntity } from '../services/database/types'; | ||||
| import Database from '../database'; | ||||
| import ActionLogger from '../utils/ActionLogger'; | ||||
|  | ||||
| export default class ResourceLocalState extends BaseModel { | ||||
| 	public static tableName() { | ||||
| @@ -34,9 +35,11 @@ export default class ResourceLocalState extends BaseModel { | ||||
| 		return this.db().transactionExecBatch(this.saveQueries(o)); | ||||
| 	} | ||||
|  | ||||
| 	public static batchDelete(ids: string[], options: any = null) { | ||||
| 		options = options ? { ...options } : {}; | ||||
| 	public static batchDelete(ids: string[], options: DeleteOptions = {}) { | ||||
| 		options = { ...options }; | ||||
| 		options.idFieldName = 'resource_id'; | ||||
| 		options.sourceDescription = ActionLogger.from(options.sourceDescription); | ||||
| 		options.sourceDescription.addDescription('Delete local resource state'); | ||||
| 		return super.batchDelete(ids, options); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { TagEntity, TagsWithNoteCountEntity } from '../services/database/types'; | ||||
|  | ||||
| import BaseModel from '../BaseModel'; | ||||
| import BaseModel, { DeleteOptions } from '../BaseModel'; | ||||
| import BaseItem from './BaseItem'; | ||||
| import NoteTag from './NoteTag'; | ||||
| import Note from './Note'; | ||||
| import { _ } from '../locale'; | ||||
| import ActionLogger from '../utils/ActionLogger'; | ||||
|  | ||||
| export default class Tag extends BaseItem { | ||||
| 	public static tableName() { | ||||
| @@ -45,14 +46,21 @@ export default class Tag extends BaseItem { | ||||
| 	public static async untagAll(tagId: string) { | ||||
| 		const noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ?', [tagId]); | ||||
| 		for (let i = 0; i < noteTags.length; i++) { | ||||
| 			await NoteTag.delete(noteTags[i].id); | ||||
| 			await NoteTag.delete(noteTags[i].id, { sourceDescription: 'untagAll/disassociate note' }); | ||||
| 		} | ||||
|  | ||||
| 		await Tag.delete(tagId); | ||||
| 		await Tag.delete(tagId, { sourceDescription: 'untagAll/delete tag' }); | ||||
| 	} | ||||
|  | ||||
| 	public static async delete(id: string, options: any = null) { | ||||
| 		if (!options) options = {}; | ||||
| 	public static async delete(id: string, options: DeleteOptions = {}) { | ||||
| 		const actionLogger = ActionLogger.from(options.sourceDescription); | ||||
| 		const tagTitle = (await Tag.load(id)).title; | ||||
| 		actionLogger.addDescription(`tag title: ${JSON.stringify(tagTitle)}`); | ||||
|  | ||||
| 		options = { | ||||
| 			...options, | ||||
| 			sourceDescription: actionLogger, | ||||
| 		}; | ||||
|  | ||||
| 		await super.delete(id, options); | ||||
|  | ||||
| @@ -93,14 +101,18 @@ export default class Tag extends BaseItem { | ||||
| 	} | ||||
|  | ||||
| 	public static async removeNote(tagId: string, noteId: string) { | ||||
| 		const tag = await Tag.load(tagId); | ||||
|  | ||||
| 		const actionLogger = ActionLogger.from(`Tag/removeNote - tag: ${tag.title}`); | ||||
|  | ||||
| 		const noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ? and note_id = ?', [tagId, noteId]); | ||||
| 		for (let i = 0; i < noteTags.length; i++) { | ||||
| 			await NoteTag.delete(noteTags[i].id); | ||||
| 			await NoteTag.delete(noteTags[i].id, { sourceDescription: actionLogger.clone() }); | ||||
| 		} | ||||
|  | ||||
| 		this.dispatch({ | ||||
| 			type: 'NOTE_TAG_REMOVE', | ||||
| 			item: await Tag.load(tagId), | ||||
| 			item: tag, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ export default async (noteIds: string[], folderIds: string[], targetFolderId: st | ||||
|  | ||||
| 	if (!targetFolder) throw new Error(`No such folder: ${targetFolderId}`); | ||||
|  | ||||
| 	const defaultDeleteOptions: DeleteOptions = { toTrash: true }; | ||||
| 	const defaultDeleteOptions: DeleteOptions = { toTrash: true, sourceDescription: 'onFolderDrop' }; | ||||
|  | ||||
| 	if (targetFolder.id !== getTrashFolderId()) { | ||||
| 		defaultDeleteOptions.toTrashParentId = targetFolder.id; | ||||
|   | ||||
| @@ -45,7 +45,7 @@ export default class AlarmService { | ||||
| 			this.logger().info(`Clearing notification for non-existing note. Alarm ${alarmIds[i]}`); | ||||
| 			await this.driver().clearNotification(alarmIds[i]); | ||||
| 		} | ||||
| 		await Alarm.batchDelete(alarmIds); | ||||
| 		await Alarm.batchDelete(alarmIds, { sourceDescription: 'AlarmService/garbageCollect' }); | ||||
| 	} | ||||
|  | ||||
| 	// When passing a note, make sure it has all the required properties | ||||
| @@ -93,7 +93,7 @@ export default class AlarmService { | ||||
| 			if (clearAlarm) { | ||||
| 				this.logger().info(`Clearing notification for note ${noteId}`); | ||||
| 				await driver.clearNotification(alarm.id); | ||||
| 				await Alarm.delete(alarm.id); | ||||
| 				await Alarm.delete(alarm.id, { sourceDescription: 'AlarmService/clearAlarm' }); | ||||
| 			} | ||||
|  | ||||
| 			if (isDeleted || !Note.needAlarm(note)) return; | ||||
|   | ||||
| @@ -24,7 +24,7 @@ export default class MigrationService extends BaseService { | ||||
|  | ||||
| 			try { | ||||
| 				await this.runScript(migration.number); | ||||
| 				await Migration.delete(migration.id); | ||||
| 				await Migration.delete(migration.id, { sourceDescription: 'MigrationService' }); | ||||
| 			} catch (error) { | ||||
| 				this.logger().error(`Cannot run migration: ${migration.number}`, error); | ||||
| 				break; | ||||
|   | ||||
| @@ -132,7 +132,7 @@ export default class ResourceService extends BaseService { | ||||
| 					await this.setAssociatedResources(note.id, note.body); | ||||
| 				} | ||||
| 			} else { | ||||
| 				await Resource.delete(resourceId); | ||||
| 				await Resource.delete(resourceId, { sourceDescription: 'deleteOrphanResources' }); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -32,7 +32,7 @@ export default async function(request: Request, id: string = null, link: string | ||||
| 	} | ||||
|  | ||||
| 	if (request.method === RequestMethod.DELETE) { | ||||
| 		await Folder.delete(id, { toTrash: request.query.permanent !== '1' }); | ||||
| 		await Folder.delete(id, { toTrash: request.query.permanent !== '1', sourceDescription: 'api/folders DELETE' }); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -511,7 +511,7 @@ export default async function(request: Request, id: string = null, link: string | ||||
| 	} | ||||
|  | ||||
| 	if (request.method === RequestMethod.DELETE) { | ||||
| 		await Note.delete(id, { toTrash: request.query.permanent !== '1' }); | ||||
| 		await Note.delete(id, { toTrash: request.query.permanent !== '1', sourceDescription: 'api/notes DELETE' }); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -36,7 +36,7 @@ export default async function(modelType: number, request: Request, id: string = | ||||
|  | ||||
| 	if (request.method === 'DELETE' && id) { | ||||
| 		const model = await getOneModel(); | ||||
| 		await ModelClass.delete(model.id); | ||||
| 		await ModelClass.delete(model.id, { source: 'API: DELETE method' }); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -204,8 +204,9 @@ export default class ShareService { | ||||
| 		// call deleteAllByShareId() | ||||
| 		await Folder.updateAllShareIds(ResourceService.instance()); | ||||
|  | ||||
| 		await Folder.delete(folderId, { deleteChildren: false, disableReadOnlyCheck: true }); | ||||
| 		await Folder.deleteAllByShareId(folder.share_id, { disableReadOnlyCheck: true, trackDeleted: false }); | ||||
| 		const source = 'ShareService.leaveSharedFolder'; | ||||
| 		await Folder.delete(folderId, { deleteChildren: false, disableReadOnlyCheck: true, sourceDescription: source }); | ||||
| 		await Folder.deleteAllByShareId(folder.share_id, { disableReadOnlyCheck: true, trackDeleted: false, sourceDescription: source }); | ||||
| 	} | ||||
|  | ||||
| 	// Finds any folder that is associated with a share, but the user no longer | ||||
|   | ||||
| @@ -33,7 +33,7 @@ export default class ItemUploader { | ||||
| 		this.maxBatchSize_ = v; | ||||
| 	} | ||||
|  | ||||
| 	public async serializeAndUploadItem(ItemClass: any, path: string, local: BaseItemEntity) { | ||||
| 	public async serializeAndUploadItem(ItemClass: typeof BaseItem, path: string, local: BaseItemEntity) { | ||||
| 		const preUploadItem = this.preUploadedItems_[path]; | ||||
| 		if (preUploadItem) { | ||||
| 			if (this.preUploadedItemUpdatedTimes_[path] !== local.updated_time) { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import { SyncAction, conflictActions } from './types'; | ||||
|  | ||||
| const logger = Logger.create('handleConflictAction'); | ||||
|  | ||||
| export default async (action: SyncAction, ItemClass: any, remoteExists: boolean, remoteContent: any, local: any, syncTargetId: number, itemIsReadOnly: boolean, dispatch: Dispatch) => { | ||||
| export default async (action: SyncAction, ItemClass: typeof BaseItem, remoteExists: boolean, remoteContent: any, local: any, syncTargetId: number, itemIsReadOnly: boolean, dispatch: Dispatch) => { | ||||
| 	if (!conflictActions.includes(action)) return; | ||||
|  | ||||
| 	logger.debug(`Handling conflict: ${action}`); | ||||
| @@ -30,6 +30,7 @@ export default async (action: SyncAction, ItemClass: any, remoteExists: boolean, | ||||
| 		} else { | ||||
| 			await ItemClass.delete(local.id, { | ||||
| 				changeSource: ItemChange.SOURCE_SYNC, | ||||
| 				sourceDescription: 'sync: handleConflictAction: non-note conflict', | ||||
| 				trackDeleted: false, | ||||
| 			}); | ||||
| 		} | ||||
| @@ -84,7 +85,14 @@ export default async (action: SyncAction, ItemClass: any, remoteExists: boolean, | ||||
| 			if (local.encryption_applied) dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' }); | ||||
| 		} else { | ||||
| 			// Remote no longer exists (note deleted) so delete local one too | ||||
| 			await ItemClass.delete(local.id, { changeSource: ItemChange.SOURCE_SYNC, trackDeleted: false }); | ||||
| 			await ItemClass.delete( | ||||
| 				local.id, | ||||
| 				{ | ||||
| 					changeSource: ItemChange.SOURCE_SYNC, | ||||
| 					trackDeleted: false, | ||||
| 					sourceDescription: 'sync: handleConflictAction: note/resource conflict', | ||||
| 				}, | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -4,9 +4,9 @@ import Note from '../../models/Note'; | ||||
|  | ||||
| export default async () => { | ||||
| 	const result = await BaseItem.allItemsInTrash(); | ||||
| 	await Note.batchDelete(result.noteIds); | ||||
| 	await Note.batchDelete(result.noteIds, { sourceDescription: 'emptyTrash/notes' }); | ||||
|  | ||||
| 	for (const folderId of result.folderIds) { | ||||
| 		await Folder.delete(folderId, { deleteChildren: false }); | ||||
| 		await Folder.delete(folderId, { deleteChildren: false, sourceDescription: 'emptyTrash/folders' }); | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -20,7 +20,7 @@ const permanentlyDeleteOldItems = async (ttl: number = null) => { | ||||
| 	const result = await Folder.trashItemsOlderThan(ttl); | ||||
| 	logger.info('Items to permanently delete:', result); | ||||
|  | ||||
| 	await Note.batchDelete(result.noteIds); | ||||
| 	await Note.batchDelete(result.noteIds, { sourceDescription: 'permanentlyDeleteOldItems' }); | ||||
|  | ||||
| 	// We only auto-delete folders if they are empty. | ||||
| 	for (const folderId of result.folderIds) { | ||||
|   | ||||
							
								
								
									
										44
									
								
								packages/lib/utils/ActionLogger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/lib/utils/ActionLogger.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
|  | ||||
| export enum ItemActionType { | ||||
| 	Delete = 'DeleteAction', | ||||
| } | ||||
|  | ||||
| const actionTypeToLogger = { | ||||
| 	[ItemActionType.Delete]: Logger.create(ItemActionType.Delete), | ||||
| }; | ||||
|  | ||||
| export default class ActionLogger { | ||||
| 	private descriptions: string[] = []; | ||||
|  | ||||
| 	private constructor(private source: string) { } | ||||
|  | ||||
| 	public clone() { | ||||
| 		const clone = new ActionLogger(this.source); | ||||
| 		clone.descriptions = [...this.descriptions]; | ||||
| 		return clone; | ||||
| 	} | ||||
|  | ||||
| 	// addDescription is used to add labels with information that may not be available | ||||
| 	// when .log is called. For example, to include the title of a deleted note. | ||||
| 	public addDescription(description: string) { | ||||
| 		this.descriptions.push(description); | ||||
| 	} | ||||
|  | ||||
| 	public log(action: ItemActionType, itemIds: string|string[]) { | ||||
| 		const logger = actionTypeToLogger[action]; | ||||
| 		logger.info(`${this.source}: ${this.descriptions.join(',')}; Item IDs: ${JSON.stringify(itemIds)}`); | ||||
| 	} | ||||
|  | ||||
| 	public static from(source: ActionLogger|string|undefined) { | ||||
| 		if (!source) { | ||||
| 			source = 'Unknown source'; | ||||
| 		} | ||||
|  | ||||
| 		if (typeof source === 'string') { | ||||
| 			return new ActionLogger(source); | ||||
| 		} | ||||
|  | ||||
| 		return source; | ||||
| 	} | ||||
| } | ||||
| @@ -181,8 +181,14 @@ class Logger { | ||||
|  | ||||
| 	public objectsToString(...object: any[]) { | ||||
| 		const output = []; | ||||
| 		for (let i = 0; i < object.length; i++) { | ||||
| 			output.push(`"${this.objectToString(object[i])}"`); | ||||
| 		if (object.length === 1) { | ||||
| 			// Quoting when there is only one argument can make the log more difficult to read, | ||||
| 			// particularly when formatting is handled elsewhere. | ||||
| 			output.push(this.objectToString(object[0])); | ||||
| 		} else { | ||||
| 			for (let i = 0; i < object.length; i++) { | ||||
| 				output.push(`"${this.objectToString(object[i])}"`); | ||||
| 			} | ||||
| 		} | ||||
| 		return output.join(', '); | ||||
| 	} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user