Skip to content
Snippets Groups Projects
Commit 03cc09d6 authored by Alexandru Munteanu's avatar Alexandru Munteanu
Browse files

feat(zip-files): add a component to handle downloading files as zip

parent cfe690a7
No related branches found
No related tags found
No related merge requests found
...@@ -21,37 +21,41 @@ const FileBackend = app => { ...@@ -21,37 +21,41 @@ const FileBackend = app => {
app.get('/api/fileZip/:fragmentId', authBearer, async (req, res) => { app.get('/api/fileZip/:fragmentId', authBearer, async (req, res) => {
const archive = archiver('zip') const archive = archiver('zip')
const { fragmentId } = req.params const { fragmentId } = req.params
const getObject = util.promisify(s3.getObject.bind(s3))
const listObjects = util.promisify(s3.listObjects.bind(s3))
archive.pipe(res) try {
res.attachment(`${fragmentId}-archive.zip`) archive.pipe(res)
res.attachment(`${fragmentId}-archive.zip`)
const params = {
Bucket: s3Config.bucket,
Prefix: `${fragmentId}`,
}
const listObjects = util.promisify(s3.listObjects.bind(s3)) const params = {
const getObject = util.promisify(s3.getObject.bind(s3)) Bucket: s3Config.bucket,
Prefix: `${fragmentId}`,
}
return listObjects(params).then(data => { const s3Items = await listObjects(params)
Promise.all( const s3Files = await Promise.all(
data.Contents.map(content => s3Items.Contents.map(content =>
getObject({ getObject({
Bucket: s3Config.bucket, Bucket: s3Config.bucket,
Key: content.Key, Key: content.Key,
}), }),
), ),
).then(files => { )
files.forEach((file, index) => {
archive.append(file.Body, { s3Files.forEach(f => {
name: `${_.get(file, 'Metadata.filetype') || archive.append(f.Body, {
'supplementary'}/${_.get(file, 'Metadata.filename') || name: `${_.get(f, 'Metadata.filetype') || 'supplementary'}/${_.get(
file.ETag}`, f,
}) 'Metadata.filename',
) || f.ETag}`,
}) })
archive.finalize()
}) })
})
archive.finalize()
} catch (err) {
res.status(err.statusCode).json({ error: err.message })
}
}) })
} }
......
import React from 'react' import React from 'react'
import { get } from 'lodash'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { get, isEmpty } from 'lodash'
import { Button, Icon } from '@pubsweet/ui' import { Button, Icon } from '@pubsweet/ui'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { compose, getContext, withHandlers } from 'recompose' import { compose, getContext } from 'recompose'
import { parseVersion, getFilesURL } from './utils' import { parseVersion } from './utils'
import ZipFiles from './ZipFiles'
const DashboardCard = ({ const DashboardCard = ({
deleteProject, deleteProject,
...@@ -15,15 +16,14 @@ const DashboardCard = ({ ...@@ -15,15 +16,14 @@ const DashboardCard = ({
version, version,
showAbstractModal, showAbstractModal,
journal, journal,
getItems,
...rest ...rest
}) => { }) => {
const { submitted, title, type, version: vers } = parseVersion(version) const { submitted, title, type, version: vers } = parseVersion(version)
const files = getFilesURL(get(version, 'files'))
const status = get(project, 'status') || 'Draft' const status = get(project, 'status') || 'Draft'
const hasFiles = !isEmpty(files)
const abstract = get(version, 'metadata.abstract') const abstract = get(version, 'metadata.abstract')
const metadata = get(version, 'metadata') const metadata = get(version, 'metadata')
const files = get(version, 'files')
const hasFiles = files ? Object.values(files).some(f => f.length > 0) : false
const journalIssueType = journal.issueTypes.find( const journalIssueType = journal.issueTypes.find(
t => t.value === get(metadata, 'issue'), t => t.value === get(metadata, 'issue'),
) )
...@@ -47,17 +47,11 @@ const DashboardCard = ({ ...@@ -47,17 +47,11 @@ const DashboardCard = ({
</ManuscriptInfo> </ManuscriptInfo>
</Left> </Left>
<Right> <Right>
{/* <form onSubmit={getItems}> <ZipFiles disabled={!hasFiles} fragmentId={version.id}>
<Icon>download</Icon> <ClickableIcon disabled={!hasFiles}>
<button type="submit">DOWNLOAD</button> <Icon>download</Icon>
</form> */} </ClickableIcon>
<ClickableIcon </ZipFiles>
disabled={!hasFiles}
// onClick={() => (hasFiles ? downloadAll(files) : null)}
onClick={getItems}
>
<Icon>download</Icon>
</ClickableIcon>
<ClickableIcon onClick={() => deleteProject(project)}> <ClickableIcon onClick={() => deleteProject(project)}>
<Icon>trash-2</Icon> <Icon>trash-2</Icon>
</ClickableIcon> </ClickableIcon>
...@@ -122,35 +116,7 @@ const DashboardCard = ({ ...@@ -122,35 +116,7 @@ const DashboardCard = ({
) : null ) : null
} }
export default compose( export default compose(getContext({ journal: PropTypes.object }))(DashboardCard)
getContext({ journal: PropTypes.object }),
connect(state => ({
token: state.currentUser.user.token,
})),
withHandlers({
getItems: ({ version, token }) => () => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function onXhrStateChange() {
if (this.readyState === 4 && this.status === 200) {
const fileName = `${version.id}-archive.zip`
const f = new File([this.response], fileName, {
type: 'application/zip',
})
const url = URL.createObjectURL(f)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
}
}
xhr.open('GET', `${window.location.origin}/api/fileZip/${version.id}`)
xhr.responseType = 'blob'
xhr.setRequestHeader('Authorization', `Bearer ${token}`)
xhr.send()
},
}),
)(DashboardCard)
// #region styled-components // #region styled-components
const defaultText = css` const defaultText = css`
......
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { compose, withHandlers, withState } from 'recompose'
import { Spinner } from '../UIComponents/index'
const createAnchorElement = (file, filename) => {
const url = URL.createObjectURL(file)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
return {
a,
url,
}
}
const removeAnchorElement = (a, url) => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
const ZipFiles = ({
disabled,
fragmentId,
fetching,
children,
downloadFiles,
}) => (
<Root onClick={!disabled ? downloadFiles : null}>
{fetching ? <Spinner /> : children}
</Root>
)
const cache = {}
const Zip = compose(
connect(state => ({
token: state.currentUser.user.token,
})),
withState('fetching', 'setFetching', false),
withHandlers({
downloadFiles: ({ fragmentId, token, setFetching, archiveName }) => () => {
if (cache[fragmentId]) {
const fileName = archiveName || `${fragmentId}-archive.zip`
const { a, url } = createAnchorElement(cache[fragmentId], fileName)
a.click()
removeAnchorElement(a, url)
} else {
setFetching(fetching => true)
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function onXhrStateChange() {
if (this.readyState === 4) {
setFetching(fetching => false)
if (this.status >= 200 && this.status < 300) {
const fileName = archiveName || `${fragmentId}-archive.zip`
const f = new File([this.response], fileName, {
type: 'application/zip',
})
cache[fragmentId] = f
const { a, url } = createAnchorElement(f, fileName)
a.click()
removeAnchorElement(a, url)
}
}
}
xhr.open('GET', `${window.location.origin}/api/fileZip/${fragmentId}`)
xhr.responseType = 'blob'
xhr.setRequestHeader('Authorization', `Bearer ${token}`)
xhr.send()
}
},
}),
)(ZipFiles)
Zip.propTypes = {
disabled: PropTypes.bool,
archiveName: PropTypes.string,
fragmentId: PropTypes.string.isRequired,
}
Zip.defaultProps = {
disabled: false,
}
export default Zip
// #region styled components
const Root = styled.div`
align-items: center;
cursor: pointer;
display: flex;
margin: 0 7px;
width: 38px;
`
// #endregion
...@@ -34,7 +34,7 @@ module.exports = { ...@@ -34,7 +34,7 @@ module.exports = {
'pubsweet-client': { 'pubsweet-client': {
API_ENDPOINT: '/api', API_ENDPOINT: '/api',
'login-redirect': '/', 'login-redirect': '/',
'redux-log': false, 'redux-log': true,
theme: process.env.PUBSWEET_THEME, theme: process.env.PUBSWEET_THEME,
}, },
'mail-transport': { 'mail-transport': {
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment