diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index f58c377a7..1044a64b3 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -1132,6 +1132,17 @@ class BNote extends AbstractBeccaEntity { .map(row => new BAttachment(row))[0]; } + /** @returns {BAttachment[]} */ + getAttachmentByRole(role) { + return sql.getRows(` + SELECT attachments.* + FROM attachments + WHERE parentId = ? + AND role = ? + AND isDeleted = 0`, [this.noteId, role]) + .map(row => new BAttachment(row)); + } + /** * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles) * diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js index a8f758ae6..fe766c358 100644 --- a/src/public/app/services/utils.js +++ b/src/public/app/services/utils.js @@ -11,7 +11,7 @@ function parseDate(str) { return new Date(Date.parse(str)); } catch (e) { - throw new Error(`Can't parse date from ${str}: ${e.stack}`); + throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`); } } diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.js index 1f5ef41f8..f7f49d8f0 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.js @@ -100,6 +100,17 @@ export default class AttachmentDetailWidget extends BasicWidget { .text(this.attachment.title); } + const {utcDateScheduledForDeletionSince} = this.attachment; + + if (utcDateScheduledForDeletionSince) { + const scheduledSinceTimestamp = utils.parseDate(utcDateScheduledForDeletionSince)?.getTime(); + const interval = 3600 * 1000; + const deletionTimestamp = scheduledSinceTimestamp + interval; + const willBeDeletedInSeconds = Math.round((deletionTimestamp - Date.now()) / 1000); + + this.$wrapper.find('.attachment-title').append(`Will be deleted in ${willBeDeletedInSeconds} seconds.`); + } + this.$wrapper.find('.attachment-details') .text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`); this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render()); diff --git a/src/services/notes.js b/src/services/notes.js index dd1540cb4..0adc81ef1 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -336,6 +336,29 @@ function protectNote(note, protect) { } } +function checkImageAttachments(note, content) { + const re = /src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image/g; + const foundAttachmentIds = new Set(); + let match; + + while (match = re.exec(content)) { + foundAttachmentIds.push(match[1]); + } + + for (const attachment of note.getAttachmentByRole('image')) { + const imageInContent = foundAttachmentIds.has(attachment.attachmentId); + + if (attachment.utcDateScheduledForDeletionSince && imageInContent) { + attachment.utcDateScheduledForDeletionSince = null; + attachment.save(); + } else if (!attachment.utcDateScheduledForDeletionSince && !imageInContent) { + attachment.utcDateScheduledForDeletionSince = dateUtils.utcNowDateTime(); + attachment.save(); + } + } +} + + function findImageLinks(content, foundLinks) { const re = /src="[^"]*api\/images\/([a-zA-Z0-9_]+)\//g; let match; @@ -556,6 +579,8 @@ function saveLinks(note, content) { content = findImageLinks(content, foundLinks); content = findInternalLinks(content, foundLinks); content = findIncludeNoteLinks(content, foundLinks); + + checkImageAttachments(note, content); } else if (note.type === 'relationMap') { findRelationMapLinks(content, foundLinks); @@ -735,11 +760,13 @@ function scanForLinks(note, content) { } try { - const newContent = saveLinks(note, content); + sql.transactional(() => { + const newContent = saveLinks(note, content); - if (content !== newContent) { - note.setContent(newContent); - } + if (content !== newContent) { + note.setContent(newContent); + } + }); } catch (e) { log.error(`Could not scan for links note ${note.noteId}: ${e.message} ${e.stack}`);