From c7d33fbd8301726d55aec1d0ad34f1dd885e3833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Vi=E1=BB=87t=20H=C6=B0ng?= Date: Tue, 27 Aug 2019 22:24:36 +1200 Subject: [PATCH] Allow user to view attachments and clear unused attachments --- .../main/lib/dataApi/attachmentManagement.js | 47 ++++++++ .../modals/PreferencesModal/StoragesTab.js | 102 +++++++++++++++++- .../modals/PreferencesModal/StoragesTab.styl | 23 +++- 3 files changed, 168 insertions(+), 4 deletions(-) diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index 9419435c..c5b715c3 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -624,6 +624,52 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey } } +function getAttachments (markdownContent, storageKey, noteKey) { + if (storageKey == null || noteKey == null || markdownContent == null) { + return + } + const targetStorage = findStorage.findStorage(storageKey) + const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent) + const attachmentsInNoteOnlyFileNames = [] + if (attachmentsInNote) { + for (let i = 0; i < attachmentsInNote.length; i++) { + attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), '')) + } + } + if (fs.existsSync(attachmentFolder)) { + return new Promise((resolve, reject) => { + fs.readdir(attachmentFolder, (err, files) => { + if (err) { + console.error('Error reading directory "' + attachmentFolder + '". Error:') + console.error(err) + reject(err) + return + } + const attachmentsNotInNotePaths = [] + const attachmentsInNotePaths = [] + const allAttachments = [] + for (const file of files) { + const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file) + if (!attachmentsInNoteOnlyFileNames.includes(file)) { + attachmentsNotInNotePaths.push(absolutePathOfFile) + } else { + attachmentsInNotePaths.push(absolutePathOfFile) + } + allAttachments.push(absolutePathOfFile) + } + resolve({ + allAttachments, + attachmentsNotInNotePaths, + attachmentsInNotePaths + }) + }) + }) + } else { + return null + } +} + /** * Clones the attachments of a given note. * Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination. @@ -728,6 +774,7 @@ module.exports = { removeStorageAndNoteReferences, deleteAttachmentFolder, deleteAttachmentsNotPresentInNote, + getAttachments, moveAttachments, cloneAttachments, isAttachmentLink, diff --git a/browser/main/modals/PreferencesModal/StoragesTab.js b/browser/main/modals/PreferencesModal/StoragesTab.js index 046b24e6..b2c3b32e 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.js +++ b/browser/main/modals/PreferencesModal/StoragesTab.js @@ -3,8 +3,10 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './StoragesTab.styl' import dataApi from 'browser/main/lib/dataApi' +import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import StorageItem from './StorageItem' import i18n from 'browser/lib/i18n' +import fs from 'fs' const electron = require('electron') const { shell, remote } = electron @@ -25,6 +27,20 @@ function browseFolder () { }) } +function humanFileSize (bytes) { + const threshold = 1000 + if (Math.abs(bytes) < threshold) { + return bytes + ' B' + } + var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + var u = -1 + do { + bytes /= threshold + ++u + } while (Math.abs(bytes) >= threshold && u < units.length - 1) + return bytes.toFixed(1) + ' ' + units[u] +} + class StoragesTab extends React.Component { constructor (props) { super(props) @@ -35,8 +51,26 @@ class StoragesTab extends React.Component { name: 'Unnamed', type: 'FILESYSTEM', path: '' - } + }, + attachments: [] } + this.loadAttachmentStorage() + } + + loadAttachmentStorage () { + const promises = [] + this.props.data.noteMap.map(note => { + const promise = attachmentManagement.getAttachments( + note.content, + note.storage, + note.key + ) + if (promise) promises.push(promise) + }) + + Promise.all(promises) + .then(data => this.setState({attachments: data})) + .catch(console.error) } handleAddStorageButton (e) { @@ -57,8 +91,61 @@ class StoragesTab extends React.Component { e.preventDefault() } + removeAllAttachments (attachments) { + const promises = [] + for (const attachment of attachments) { + for (const file of attachment) { + const promise = new Promise((resolve, reject) => { + fs.unlink(file, (err) => { + if (err) { + console.error('Could not delete "%s"', file) + console.error(err) + reject(err) + return + } + resolve() + }) + }) + promises.push(promise) + } + } + Promise.all(promises) + .then(() => this.loadAttachmentStorage()) + .catch(console.error) + } + renderList () { const { data, boundingBox } = this.props + const { attachments } = this.state + + const totalUnusedAttachments = attachments + .reduce((acc, curr) => acc + curr.attachmentsNotInNotePaths.length, 0) + const totalInuseAttachments = attachments + .reduce((acc, curr) => acc + curr.attachmentsInNotePaths.length, 0) + const totalAttachments = totalUnusedAttachments + totalInuseAttachments + + const unusedAttachments = attachments.reduce((acc, curr) => { + acc.push(curr.attachmentsNotInNotePaths) + return acc + }, []) + + const totalUnusedAttachmentsSize = attachments + .reduce((acc, curr) => { + return acc + curr.attachmentsNotInNotePaths.reduce((racc, rcurr) => { + const stats = fs.statSync(rcurr) + const fileSizeInBytes = stats.size + return racc + fileSizeInBytes + }, 0) + }, 0) + const totalInuseAttachmentsSize = attachments + .reduce((acc, curr) => { + return acc + curr.attachmentsInNotePaths.reduce((racc, rcurr) => { + const stats = fs.statSync(rcurr) + const fileSizeInBytes = stats.size + return racc + fileSizeInBytes + }, 0) + }, 0) + const totalAttachmentsSize = totalUnusedAttachmentsSize + totalInuseAttachmentsSize if (!boundingBox) { return null } const storageList = data.storageMap.map((storage) => { @@ -82,6 +169,19 @@ class StoragesTab extends React.Component { {i18n.__('Add Storage Location')} +
{i18n.__('Attachment storage')}
+

+ Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ({totalUnusedAttachments} items) +

+

+ In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ({totalInuseAttachments} items) +

+

+ Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} items) +

+ ) } diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index b63cc85e..fbfa89e6 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -33,6 +33,17 @@ colorDefaultButton() font-size $tab--button-font-size border-radius 2px +.list-attachment-label + margin-bottom 10px + color $ui-text-color +.list-attachement-clear-button + height 30px + border none + border-top-right-radius 2px + border-bottom-right-radius 2px + colorPrimaryButton() + vertical-align middle + padding 0 20px .addStorage margin-bottom 15px @@ -154,8 +165,8 @@ body[data-theme="dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-dark-borderColor - - + .list-attachement-clear-button + colorDarkPrimaryButton() body[data-theme="solarized-dark"] .root @@ -194,6 +205,8 @@ body[data-theme="solarized-dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-solarized-dark-borderColor + .list-attachement-clear-button + colorSolarizedDarkPrimaryButton() body[data-theme="monokai"] .root @@ -232,6 +245,8 @@ body[data-theme="monokai"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-monokai-borderColor + .list-attachement-clear-button + colorMonokaiPrimaryButton() body[data-theme="dracula"] .root @@ -269,4 +284,6 @@ body[data-theme="dracula"] colorDarkPrimaryButton() .addStorage-body-control-cancelButton colorDarkDefaultButton() - border-color $ui-dracula-borderColor \ No newline at end of file + border-color $ui-dracula-borderColor + .list-attachement-clear-button + colorDraculaPrimaryButton() \ No newline at end of file