albums: add download menu

cap
Varun Patil 2022-12-06 11:38:57 -08:00
parent b2ad076c06
commit 2011433536
8 changed files with 112 additions and 23 deletions

View File

@ -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

View File

@ -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'],

View File

@ -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);
}
} }

View File

@ -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

View 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()
{ {

View File

@ -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>

View File

@ -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 }));
} }

View File

@ -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);
} }
/** /**