albums: add download menu
parent
b2ad076c06
commit
2011433536
|
@ -6,6 +6,7 @@ This file is manually updated. Please file an issue if something is missing.
|
||||||
|
|
||||||
- **Feature**: Slideshow for photos and videos
|
- **Feature**: Slideshow for photos and videos
|
||||||
- **Feature**: Support for GPU transcoding
|
- **Feature**: Support for GPU transcoding
|
||||||
|
- **Feature**: Allow downloading entire albums
|
||||||
- Fixed support for HEVC live photos
|
- Fixed support for HEVC live photos
|
||||||
- Fixed native photo sharing
|
- Fixed native photo sharing
|
||||||
- Use larger previews in viewer
|
- Use larger previews in viewer
|
||||||
|
|
|
@ -45,6 +45,7 @@ return [
|
||||||
['name' => 'Days#dayPost', 'url' => '/api/days', 'verb' => 'POST'],
|
['name' => 'Days#dayPost', 'url' => '/api/days', 'verb' => 'POST'],
|
||||||
|
|
||||||
['name' => 'Albums#albums', 'url' => '/api/albums', 'verb' => 'GET'],
|
['name' => 'Albums#albums', 'url' => '/api/albums', 'verb' => 'GET'],
|
||||||
|
['name' => 'Albums#download', 'url' => '/api/albums/download', 'verb' => 'POST'],
|
||||||
|
|
||||||
['name' => 'Tags#tags', 'url' => '/api/tags', 'verb' => 'GET'],
|
['name' => 'Tags#tags', 'url' => '/api/tags', 'verb' => 'GET'],
|
||||||
['name' => 'Tags#preview', 'url' => '/api/tags/preview/{tag}', 'verb' => 'GET'],
|
['name' => 'Tags#preview', 'url' => '/api/tags/preview/{tag}', 'verb' => 'GET'],
|
||||||
|
|
|
@ -36,15 +36,10 @@ class AlbumsController extends ApiBase
|
||||||
public function albums(int $t = 0): JSONResponse
|
public function albums(int $t = 0): JSONResponse
|
||||||
{
|
{
|
||||||
$user = $this->userSession->getUser();
|
$user = $this->userSession->getUser();
|
||||||
if (null === $user) {
|
if (null === $user || !$this->albumsIsEnabled()) {
|
||||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check tags enabled for this user
|
|
||||||
if (!$this->albumsIsEnabled()) {
|
|
||||||
return new JSONResponse(['message' => 'Albums not enabled for user'], Http::STATUS_PRECONDITION_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run actual query
|
// Run actual query
|
||||||
$list = [];
|
$list = [];
|
||||||
if ($t & 1) { // personal
|
if ($t & 1) { // personal
|
||||||
|
@ -56,4 +51,34 @@ class AlbumsController extends ApiBase
|
||||||
|
|
||||||
return new JSONResponse($list, Http::STATUS_OK);
|
return new JSONResponse($list, Http::STATUS_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* Download an album as a zip file
|
||||||
|
*/
|
||||||
|
public function download(string $name = ''): JSONResponse
|
||||||
|
{
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (null === $user || !$this->albumsIsEnabled()) {
|
||||||
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get album
|
||||||
|
$album = $this->timelineQuery->getAlbumIfAllowed($user->getUID(), $name);
|
||||||
|
if (null === $album) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get files
|
||||||
|
$files = $this->timelineQuery->getAlbumFiles($album['album_id']);
|
||||||
|
if (empty($files)) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get download handle
|
||||||
|
$handle = \OCA\Memories\Controller\DownloadController::createHandle($files);
|
||||||
|
|
||||||
|
return new JSONResponse(['handle' => $handle], Http::STATUS_OK);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,15 +46,21 @@ class DownloadController extends ApiBase
|
||||||
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store in session
|
|
||||||
$session = \OC::$server->get(ISession::class);
|
|
||||||
|
|
||||||
// Generate random id
|
|
||||||
$handle = \OC::$server->get(ISecureRandom::class)->generate(16, ISecureRandom::CHAR_ALPHANUMERIC);
|
|
||||||
$session->set("memories_download_ids_{$handle}", $files);
|
|
||||||
|
|
||||||
// Return id
|
// Return id
|
||||||
return new JSONResponse(['handle' => $handle]);
|
return new JSONResponse(['handle' => $this->createHandle($files)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a handle for downloading files.
|
||||||
|
*
|
||||||
|
* @param int[] $files
|
||||||
|
*/
|
||||||
|
public static function createHandle(array $files): string
|
||||||
|
{
|
||||||
|
$handle = \OC::$server->get(ISecureRandom::class)->generate(16, ISecureRandom::CHAR_ALPHANUMERIC);
|
||||||
|
\OC::$server->get(ISession::class)->set("memories_download_ids_{$handle}", $files);
|
||||||
|
|
||||||
|
return $handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,12 +159,16 @@ class DownloadController extends ApiBase
|
||||||
/** @var ?File */
|
/** @var ?File */
|
||||||
$file = null;
|
$file = null;
|
||||||
|
|
||||||
|
/** @var ?string */
|
||||||
|
$name = (string) $fileId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This checks permissions
|
// This checks permissions
|
||||||
$file = $this->getUserFile($fileId);
|
$file = $this->getUserFile($fileId);
|
||||||
if (null === $file) {
|
if (null === $file) {
|
||||||
throw new \Exception('File not found');
|
throw new \Exception('File not found');
|
||||||
}
|
}
|
||||||
|
$name = $file->getName();
|
||||||
|
|
||||||
// Open file
|
// Open file
|
||||||
$handle = $file->fopen('rb');
|
$handle = $file->fopen('rb');
|
||||||
|
@ -167,9 +177,8 @@ class DownloadController extends ApiBase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle duplicate names
|
// Handle duplicate names
|
||||||
$name = $file->getName();
|
|
||||||
if (isset($nameCounts[$name])) {
|
if (isset($nameCounts[$name])) {
|
||||||
$nameCounts[$name] += 1;
|
++$nameCounts[$name];
|
||||||
|
|
||||||
// add count before extension
|
// add count before extension
|
||||||
$extpos = strrpos($name, '.');
|
$extpos = strrpos($name, '.');
|
||||||
|
@ -192,9 +201,6 @@ class DownloadController extends ApiBase
|
||||||
throw new \Exception('Failed to add file to zip');
|
throw new \Exception('Failed to add file to zip');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Let the user know that something went wrong
|
|
||||||
$name = $file->getName() ?: (string) $fileId;
|
|
||||||
|
|
||||||
// create a dummy memory file with the error message
|
// create a dummy memory file with the error message
|
||||||
$dummy = fopen('php://memory', 'rw+');
|
$dummy = fopen('php://memory', 'rw+');
|
||||||
fwrite($dummy, $e->getMessage());
|
fwrite($dummy, $e->getMessage());
|
||||||
|
@ -204,7 +210,7 @@ class DownloadController extends ApiBase
|
||||||
$dummy,
|
$dummy,
|
||||||
"{$name}_error.txt",
|
"{$name}_error.txt",
|
||||||
\strlen($e->getMessage()),
|
\strlen($e->getMessage()),
|
||||||
$file->getMTime(),
|
time(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// close the dummy file
|
// close the dummy file
|
||||||
|
|
|
@ -155,7 +155,7 @@ trait TimelineQueryAlbums
|
||||||
* @param string $uid UID of CURRENT user
|
* @param string $uid UID of CURRENT user
|
||||||
* @param string $albumId $user/$name where $user is the OWNER of the album
|
* @param string $albumId $user/$name where $user is the OWNER of the album
|
||||||
*/
|
*/
|
||||||
private function getAlbumIfAllowed(string $uid, string $albumId)
|
public function getAlbumIfAllowed(string $uid, string $albumId)
|
||||||
{
|
{
|
||||||
// Split name and uid
|
// Split name and uid
|
||||||
$parts = explode('/', $albumId);
|
$parts = explode('/', $albumId);
|
||||||
|
@ -197,6 +197,26 @@ trait TimelineQueryAlbums
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full list of fileIds in album.
|
||||||
|
*/
|
||||||
|
public function getAlbumFiles(int $albumId)
|
||||||
|
{
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
$query->select('file_id')->from('photos_albums_files', 'paf')->where(
|
||||||
|
$query->expr()->eq('album_id', $query->createNamedParameter($albumId, IQueryBuilder::PARAM_INT))
|
||||||
|
);
|
||||||
|
$query->innerJoin('paf', 'filecache', 'fc', $query->expr()->eq('fc.fileid', 'paf.file_id'));
|
||||||
|
|
||||||
|
$fileIds = [];
|
||||||
|
$result = $query->executeQuery();
|
||||||
|
while ($row = $result->fetch()) {
|
||||||
|
$fileIds[] = (int) $row['file_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fileIds;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the name of the collaborators table */
|
/** Get the name of the collaborators table */
|
||||||
private function collaboratorsTable()
|
private function collaboratorsTable()
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,6 +29,15 @@
|
||||||
{{ t("memories", "Share album") }}
|
{{ t("memories", "Share album") }}
|
||||||
<template #icon> <ShareIcon :size="20" /> </template>
|
<template #icon> <ShareIcon :size="20" /> </template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
|
<NcActionButton
|
||||||
|
:aria-label="t('memories', 'Download album')"
|
||||||
|
@click="downloadAlbum()"
|
||||||
|
close-after-click
|
||||||
|
v-if="!isAlbumList"
|
||||||
|
>
|
||||||
|
{{ t("memories", "Download album") }}
|
||||||
|
<template #icon> <DownloadIcon :size="20" /> </template>
|
||||||
|
</NcActionButton>
|
||||||
<NcActionButton
|
<NcActionButton
|
||||||
:aria-label="t('memories', 'Edit album details')"
|
:aria-label="t('memories', 'Edit album details')"
|
||||||
@click="$refs.createModal.open(true)"
|
@click="$refs.createModal.open(true)"
|
||||||
|
@ -65,16 +74,21 @@ import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
||||||
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
||||||
import NcActionCheckbox from "@nextcloud/vue/dist/Components/NcActionCheckbox";
|
import NcActionCheckbox from "@nextcloud/vue/dist/Components/NcActionCheckbox";
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
import { getCurrentUser } from "@nextcloud/auth";
|
||||||
|
import axios from "@nextcloud/axios";
|
||||||
|
|
||||||
import AlbumCreateModal from "../modal/AlbumCreateModal.vue";
|
import AlbumCreateModal from "../modal/AlbumCreateModal.vue";
|
||||||
import AlbumDeleteModal from "../modal/AlbumDeleteModal.vue";
|
import AlbumDeleteModal from "../modal/AlbumDeleteModal.vue";
|
||||||
import AlbumShareModal from "../modal/AlbumShareModal.vue";
|
import AlbumShareModal from "../modal/AlbumShareModal.vue";
|
||||||
|
|
||||||
|
import { downloadWithHandle } from "../../services/dav/download";
|
||||||
|
|
||||||
import BackIcon from "vue-material-design-icons/ArrowLeft.vue";
|
import BackIcon from "vue-material-design-icons/ArrowLeft.vue";
|
||||||
|
import DownloadIcon from "vue-material-design-icons/Download.vue";
|
||||||
import EditIcon from "vue-material-design-icons/Pencil.vue";
|
import EditIcon from "vue-material-design-icons/Pencil.vue";
|
||||||
import DeleteIcon from "vue-material-design-icons/Close.vue";
|
import DeleteIcon from "vue-material-design-icons/Close.vue";
|
||||||
import PlusIcon from "vue-material-design-icons/Plus.vue";
|
import PlusIcon from "vue-material-design-icons/Plus.vue";
|
||||||
import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
|
import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
|
||||||
|
import { API } from "../../services/API";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -87,6 +101,7 @@ import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
|
||||||
AlbumShareModal,
|
AlbumShareModal,
|
||||||
|
|
||||||
BackIcon,
|
BackIcon,
|
||||||
|
DownloadIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
@ -122,6 +137,15 @@ export default class AlbumTopMatter extends Mixins(GlobalMixin, UserConfig) {
|
||||||
back() {
|
back() {
|
||||||
this.$router.push({ name: "albums" });
|
this.$router.push({ name: "albums" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadAlbum() {
|
||||||
|
const res = await axios.post(
|
||||||
|
API.ALBUM_DOWNLOAD(this.$route.params.user, this.$route.params.name)
|
||||||
|
);
|
||||||
|
if (res.status === 200 && res.data.handle) {
|
||||||
|
downloadWithHandle(res.data.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,10 @@ export class API {
|
||||||
return gen(`${BASE}/albums?t=${t}`);
|
return gen(`${BASE}/albums?t=${t}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ALBUM_DOWNLOAD(user: string, name: string) {
|
||||||
|
return gen(`${BASE}/albums/download?name={user}/{name}`, { user, name });
|
||||||
|
}
|
||||||
|
|
||||||
static TAG_LIST() {
|
static TAG_LIST() {
|
||||||
return gen(`${BASE}/tags`);
|
return gen(`${BASE}/tags`);
|
||||||
}
|
}
|
||||||
|
@ -91,7 +95,7 @@ export class API {
|
||||||
return tok(gen(`${BASE}/download`));
|
return tok(gen(`${BASE}/download`));
|
||||||
}
|
}
|
||||||
|
|
||||||
static DOWNLOAD_FILE(handle: number) {
|
static DOWNLOAD_FILE(handle: string) {
|
||||||
return tok(gen(`${BASE}/download/{handle}`, { handle }));
|
return tok(gen(`${BASE}/download/{handle}`, { handle }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,15 @@ export async function downloadFiles(fileIds: number[]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = API.DOWNLOAD_FILE(res.data.handle);
|
downloadWithHandle(res.data.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download files with a download handle
|
||||||
|
* @param handle Download handle
|
||||||
|
*/
|
||||||
|
export function downloadWithHandle(handle: string) {
|
||||||
|
window.location.href = API.DOWNLOAD_FILE(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue