--- /dev/null
+/**
+ * @copyright 2022 Louis Chemineau <mlouis@chmn.me>
+ *
+ * @author Louis Chemineau <mlouis@chmn.me>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { createClient, getPatcher } from 'webdav'
+import { generateRemoteUrl } from '@nextcloud/router'
+import axios from '@nextcloud/axios'
+
+const rootPath = 'dav'
+
+// force our axios
+const patcher = getPatcher()
+patcher.patch('request', axios)
+
+// init webdav client on default dav endpoint
+const remote = generateRemoteUrl(rootPath)
+export default createClient(remote)
--- /dev/null
+/**
+ * @copyright Copyright (c) 2019 Louis Chmn <louis@chmn.me>
+ *
+ * @author Louis Chmn <louis@chmn.me>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export default `<?xml version="1.0"?>
+<d:propfind xmlns:d="DAV:"
+ xmlns:oc="http://owncloud.org/ns"
+ xmlns:nc="http://nextcloud.org/ns"
+ xmlns:ocs="http://open-collaboration-services.org/ns">
+ <d:prop>
+ <d:getcontentlength />
+ <d:getcontenttype />
+ <d:getlastmodified />
+ </d:prop>
+</d:propfind>`
--- /dev/null
+/**
+ * @copyright 2022 Louis Chemineau <mlouis@chmn.me>
+ *
+ * @author Louis Chemineau <mlouis@chmn.me>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { getLoggerBuilder } from '@nextcloud/logger'
+
+export default getLoggerBuilder()
+ .setApp('files_version')
+ .detectUser()
+ .build()
--- /dev/null
+/**
+ * @copyright 2022 Louis Chemineau <mlouis@chmn.me>
+ *
+ * @author Louis Chemineau <mlouis@chmn.me>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { getCurrentUser } from '@nextcloud/auth'
+import client from '../utils/davClient.js'
+import davRequest from '../utils/davRequest.js'
+import logger from '../utils/logger.js'
+import { basename, joinPaths } from '@nextcloud/paths'
+import { generateUrl } from '@nextcloud/router'
+import { translate } from '@nextcloud/l10n'
+import moment from '@nextcloud/moment'
+
+/**
+ * @typedef {Object} Version
+ * @property {string} title - 'Current version' or ''
+ * @property {string} fileName - File name relative to the version DAV endpoint
+ * @property {string} mimeType - Empty for the current version, else the actual mime type of the version
+ * @property {string} size - Human readable size
+ * @property {string} type - 'file'
+ * @property {number} mtime - Version creation date as a timestamp
+ * @property {string} preview - Preview URL of the version
+ * @property {string} url - Download URL of the version
+ * @property {string|null} fileVersion - The version id, null for the current version
+ * @property {boolean} isCurrent - Whether this is the current version of the file
+ */
+
+/**
+ * @param fileInfo
+ * @return {Promise<Version[]>}
+ */
+export async function fetchVersions(fileInfo) {
+ const path = `/versions/${getCurrentUser()?.uid}/versions/${fileInfo.id}`
+
+ try {
+ /** @type {import('webdav').FileStat[]} */
+ const response = await client.getDirectoryContents(path, {
+ data: davRequest,
+ })
+ return response.map(version => formatVersion(version, fileInfo))
+ } catch (exception) {
+ logger.error('Could not fetch version', { exception })
+ throw exception
+ }
+}
+
+/**
+ * Restore the given version
+ *
+ * @param {Version} version
+ * @param {object} fileInfo
+ */
+export async function restoreVersion(version, fileInfo) {
+ try {
+ logger.debug('Restoring version', { url: version.url })
+ await client.moveFile(
+ `/versions/${getCurrentUser()?.uid}/versions/${fileInfo.id}/${version.fileVersion}`,
+ `/versions/${getCurrentUser()?.uid}/restore/target`
+ )
+ } catch (exception) {
+ logger.error('Could not restore version', { exception })
+ throw exception
+ }
+}
+
+/**
+ * Format version
+ *
+ * @param {object} version - raw version received from the versions DAV endpoint
+ * @param {object} fileInfo - file properties received from the files DAV endpoint
+ * @return {Version}
+ */
+function formatVersion(version, fileInfo) {
+ const isCurrent = version.mime === ''
+ const fileVersion = isCurrent ? null : basename(version.filename)
+
+ let url = null
+ let preview = null
+
+ if (isCurrent) {
+ // https://nextcloud_server2.test/remote.php/webdav/welcome.txt?downloadStartSecret=hl5awd7tbzg
+ url = joinPaths('/remote.php/webdav', fileInfo.path, fileInfo.name)
+ preview = generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0', {
+ fileId: fileInfo.id,
+ fileEtag: fileInfo.etag,
+ })
+ } else {
+ url = joinPaths('/remote.php/dav', version.filename)
+ preview = generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}', {
+ file: joinPaths(fileInfo.path, fileInfo.name),
+ fileVersion,
+ })
+ }
+
+ return {
+ title: isCurrent ? translate('files_versions', 'Current version') : '',
+ fileName: version.filename,
+ mimeType: version.mime,
+ size: isCurrent ? fileInfo.size : version.size,
+ type: version.type,
+ mtime: moment(isCurrent ? fileInfo.mtime : version.lastmod).unix(),
+ preview,
+ url,
+ fileVersion,
+ isCurrent,
+ }
+}
<div>
<ul>
<NcListItem v-for="version in versions"
- :key="version.dateTime.unix()"
+ :key="version.mtime"
class="version"
:title="version.title"
:href="version.url">
alt=""
height="256"
width="256"
- class="version-image">
+ class="version__image">
</template>
<template #subtitle>
- <div class="version-info">
- <a v-tooltip="version.dateTime" :href="version.url">{{ version.relativeTime }}</a>
- <span class="version-info-size">•</span>
- <span class="version-info-size">
- {{ version.size }}
- </span>
+ <div class="version__info">
+ <span>{{ version.mtime | humanDateFromNow }}</span>
+ <!-- Separate dot to improve alignement -->
+ <span class="version__info__size">•</span>
+ <span class="version__info__size">{{ version.size | humanReadableSize }}</span>
</div>
</template>
- <template #actions>
- <NcActionLink :href="version.url">
+ <template v-if="!version.isCurrent" #actions>
+ <NcActionLink :href="version.url"
+ :download="version.url">
<template #icon>
<Download :size="22" />
</template>
{{ t('files_versions', 'Download version') }}
</NcActionLink>
- <NcActionButton v-if="!version.isCurrent" @click="restoreVersion(version)">
+ <NcActionButton @click="restoreVersion(version)">
<template #icon>
<BackupRestore :size="22" />
</template>
</template>
<script>
-import { createClient, getPatcher } from 'webdav'
-import axios from '@nextcloud/axios'
-import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
-import { getCurrentUser } from '@nextcloud/auth'
import BackupRestore from 'vue-material-design-icons/BackupRestore.vue'
import Download from 'vue-material-design-icons/Download.vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import { showError, showSuccess } from '@nextcloud/dialogs'
+import { fetchVersions, restoreVersion } from '../utils/versions.js'
import moment from '@nextcloud/moment'
-import { basename, joinPaths } from '@nextcloud/paths'
-import { getLoggerBuilder } from '@nextcloud/logger'
-import { translate } from '@nextcloud/l10n'
-
-const logger = getLoggerBuilder()
- .setApp('files_version')
- .detectUser()
- .build()
-
-/**
- * Get WebDAV request body for version list
- */
-function getDavRequest() {
- return `<?xml version="1.0"?>
- <d:propfind xmlns:d="DAV:"
- xmlns:oc="http://owncloud.org/ns"
- xmlns:nc="http://nextcloud.org/ns"
- xmlns:ocs="http://open-collaboration-services.org/ns">
- <d:prop>
- <d:getcontentlength />
- <d:getcontenttype />
- <d:getlastmodified />
- </d:prop>
- </d:propfind>`
-}
-
-/**
- * Format version
- *
- * @param version
- * @param fileInfo
- */
-function formatVersion(version, fileInfo) {
- const fileVersion = basename(version.filename)
- const isCurrent = version.mime === ''
-
- const preview = isCurrent
- ? generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0', {
- fileId: fileInfo.id,
- fileEtag: fileInfo.etag,
- })
- : generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}', {
- file: joinPaths(fileInfo.path, fileInfo.name),
- fileVersion,
- })
-
- return {
- displayVersionName: fileVersion,
- title: isCurrent ? translate('files_versions', 'Current version') : '',
- fileName: version.filename,
- mimeType: version.mime,
- size: OC.Util.humanFileSize(isCurrent ? fileInfo.size : version.size),
- type: version.type,
- dateTime: moment(isCurrent ? fileInfo.mtime : version.lastmod),
- relativeTime: moment(isCurrent ? fileInfo.mtime : version.lastmod).fromNow(),
- preview,
- url: isCurrent ? joinPaths('/remote.php/dav', version.filename) : joinPaths('/remote.php/dav', fileInfo.path, fileInfo.name),
- fileVersion,
- isCurrent,
- }
-}
-
-const rootPath = 'dav'
-
-// force our axios
-const patcher = getPatcher()
-patcher.patch('request', axios)
-
-// init webdav client on default dav endpoint
-const remote = generateRemoteUrl(rootPath)
-const client = createClient(remote)
export default {
name: 'VersionTab',
BackupRestore,
Download,
},
+ filters: {
+ humanReadableSize(bytes) {
+ return OC.Util.humanFileSize(bytes)
+ },
+ humanDateFromNow(timestamp) {
+ return moment(timestamp * 1000).fromNow()
+ },
+ },
data() {
-
return {
fileInfo: null,
+ /** @type {import('../utils/versions.js').Version[]} */
versions: [],
- loading: true,
+ loading: false,
}
},
methods: {
* Get the existing versions infos
*/
async fetchVersions() {
- const path = `/versions/${getCurrentUser().uid}/versions/${this.fileInfo.id}`
-
try {
- const response = await client.getDirectoryContents(path, {
- data: getDavRequest(),
- })
- this.versions = response.map(version => formatVersion(version, this.fileInfo))
- this.loading = false
- } catch (exception) {
- logger.error('Could not fetch version', { exception })
+ this.loading = true
+ this.versions = await fetchVersions(this.fileInfo)
+ } finally {
this.loading = false
}
},
*/
async restoreVersion(version) {
try {
- logger.debug('restoring version', version.url)
- await client.moveFile(
- `/versions/${getCurrentUser().uid}/versions/${this.fileInfo.id}/${version.fileVersion}`,
- `/versions/${getCurrentUser().uid}/restore/target`
- )
+ await restoreVersion(version, this.fileInfo)
+ // File info is not updated so we manually update its size and mtime if the restoration went fine.
+ this.fileInfo.size = version.size
+ this.fileInfo.mtime = version.lastmod
showSuccess(t('files_versions', 'Version restored'))
await this.fetchVersions()
} catch (exception) {
- logger.error('Could not restore version', { exception })
showError(t('files_versions', 'Could not restore version'))
}
},
.version {
display: flex;
flex-direction: row;
- &-info {
+
+ &__info {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
- &-size {
+
+ &__size {
color: var(--color-text-lighter);
}
}
- &-image {
+
+ &__image {
width: 3rem;
height: 3rem;
border: 1px solid var(--color-border);