mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Allow user to view attachments and clear unused attachments
This commit is contained in:
committed by
Junyoung Choi
parent
cf324d93fe
commit
c7d33fbd83
@@ -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.
|
* 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.
|
* 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,
|
removeStorageAndNoteReferences,
|
||||||
deleteAttachmentFolder,
|
deleteAttachmentFolder,
|
||||||
deleteAttachmentsNotPresentInNote,
|
deleteAttachmentsNotPresentInNote,
|
||||||
|
getAttachments,
|
||||||
moveAttachments,
|
moveAttachments,
|
||||||
cloneAttachments,
|
cloneAttachments,
|
||||||
isAttachmentLink,
|
isAttachmentLink,
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StoragesTab.styl'
|
import styles from './StoragesTab.styl'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
import StorageItem from './StorageItem'
|
import StorageItem from './StorageItem'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { shell, remote } = 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 {
|
class StoragesTab extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -35,8 +51,26 @@ class StoragesTab extends React.Component {
|
|||||||
name: 'Unnamed',
|
name: 'Unnamed',
|
||||||
type: 'FILESYSTEM',
|
type: 'FILESYSTEM',
|
||||||
path: ''
|
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) {
|
handleAddStorageButton (e) {
|
||||||
@@ -57,8 +91,61 @@ class StoragesTab extends React.Component {
|
|||||||
e.preventDefault()
|
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 () {
|
renderList () {
|
||||||
const { data, boundingBox } = this.props
|
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 }
|
if (!boundingBox) { return null }
|
||||||
const storageList = data.storageMap.map((storage) => {
|
const storageList = data.storageMap.map((storage) => {
|
||||||
@@ -82,6 +169,19 @@ class StoragesTab extends React.Component {
|
|||||||
<i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
|
<i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div styleName='header'>{i18n.__('Attachment storage')}</div>
|
||||||
|
<p styleName='list-attachment-label'>
|
||||||
|
Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ({totalUnusedAttachments} items)
|
||||||
|
</p>
|
||||||
|
<p styleName='list-attachment-label'>
|
||||||
|
In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ({totalInuseAttachments} items)
|
||||||
|
</p>
|
||||||
|
<p styleName='list-attachment-label'>
|
||||||
|
Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} items)
|
||||||
|
</p>
|
||||||
|
<button styleName='list-attachement-clear-button' onClick={() => this.removeAllAttachments(unusedAttachments)}>
|
||||||
|
{i18n.__('Clear unused attachments')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
colorDefaultButton()
|
colorDefaultButton()
|
||||||
font-size $tab--button-font-size
|
font-size $tab--button-font-size
|
||||||
border-radius 2px
|
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
|
.addStorage
|
||||||
margin-bottom 15px
|
margin-bottom 15px
|
||||||
@@ -154,8 +165,8 @@ body[data-theme="dark"]
|
|||||||
.addStorage-body-control-cancelButton
|
.addStorage-body-control-cancelButton
|
||||||
colorDarkDefaultButton()
|
colorDarkDefaultButton()
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
|
.list-attachement-clear-button
|
||||||
|
colorDarkPrimaryButton()
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
.root
|
.root
|
||||||
@@ -194,6 +205,8 @@ body[data-theme="solarized-dark"]
|
|||||||
.addStorage-body-control-cancelButton
|
.addStorage-body-control-cancelButton
|
||||||
colorDarkDefaultButton()
|
colorDarkDefaultButton()
|
||||||
border-color $ui-solarized-dark-borderColor
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
.list-attachement-clear-button
|
||||||
|
colorSolarizedDarkPrimaryButton()
|
||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
.root
|
.root
|
||||||
@@ -232,6 +245,8 @@ body[data-theme="monokai"]
|
|||||||
.addStorage-body-control-cancelButton
|
.addStorage-body-control-cancelButton
|
||||||
colorDarkDefaultButton()
|
colorDarkDefaultButton()
|
||||||
border-color $ui-monokai-borderColor
|
border-color $ui-monokai-borderColor
|
||||||
|
.list-attachement-clear-button
|
||||||
|
colorMonokaiPrimaryButton()
|
||||||
|
|
||||||
body[data-theme="dracula"]
|
body[data-theme="dracula"]
|
||||||
.root
|
.root
|
||||||
@@ -270,3 +285,5 @@ body[data-theme="dracula"]
|
|||||||
.addStorage-body-control-cancelButton
|
.addStorage-body-control-cancelButton
|
||||||
colorDarkDefaultButton()
|
colorDarkDefaultButton()
|
||||||
border-color $ui-dracula-borderColor
|
border-color $ui-dracula-borderColor
|
||||||
|
.list-attachement-clear-button
|
||||||
|
colorDraculaPrimaryButton()
|
||||||
Reference in New Issue
Block a user