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**: Support for GPU transcoding
- **Feature**: Allow downloading entire albums
- Fixed support for HEVC live photos
- Fixed native photo sharing
- Use larger previews in viewer

View File

@ -45,6 +45,7 @@ return [
['name' => 'Days#dayPost', 'url' => '/api/days', 'verb' => 'POST'],
['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#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
{
$user = $this->userSession->getUser();
if (null === $user) {
if (null === $user || !$this->albumsIsEnabled()) {
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
$list = [];
if ($t & 1) { // personal
@ -56,4 +51,34 @@ class AlbumsController extends ApiBase
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);
}
// 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 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 */
$file = null;
/** @var ?string */
$name = (string) $fileId;
try {
// This checks permissions
$file = $this->getUserFile($fileId);
if (null === $file) {
throw new \Exception('File not found');
}
$name = $file->getName();
// Open file
$handle = $file->fopen('rb');
@ -167,9 +177,8 @@ class DownloadController extends ApiBase
}
// Handle duplicate names
$name = $file->getName();
if (isset($nameCounts[$name])) {
$nameCounts[$name] += 1;
++$nameCounts[$name];
// add count before extension
$extpos = strrpos($name, '.');
@ -192,9 +201,6 @@ class DownloadController extends ApiBase
throw new \Exception('Failed to add file to zip');
}
} 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
$dummy = fopen('php://memory', 'rw+');
fwrite($dummy, $e->getMessage());
@ -204,7 +210,7 @@ class DownloadController extends ApiBase
$dummy,
"{$name}_error.txt",
\strlen($e->getMessage()),
$file->getMTime(),
time(),
);
// close the dummy file

View File

@ -155,7 +155,7 @@ trait TimelineQueryAlbums
* @param string $uid UID of CURRENT user
* @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
$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 */
private function collaboratorsTable()
{

View File

@ -29,6 +29,15 @@
{{ t("memories", "Share album") }}
<template #icon> <ShareIcon :size="20" /> </template>
</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
:aria-label="t('memories', 'Edit album details')"
@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 NcActionCheckbox from "@nextcloud/vue/dist/Components/NcActionCheckbox";
import { getCurrentUser } from "@nextcloud/auth";
import axios from "@nextcloud/axios";
import AlbumCreateModal from "../modal/AlbumCreateModal.vue";
import AlbumDeleteModal from "../modal/AlbumDeleteModal.vue";
import AlbumShareModal from "../modal/AlbumShareModal.vue";
import { downloadWithHandle } from "../../services/dav/download";
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 DeleteIcon from "vue-material-design-icons/Close.vue";
import PlusIcon from "vue-material-design-icons/Plus.vue";
import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
import { API } from "../../services/API";
@Component({
components: {
@ -87,6 +101,7 @@ import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
AlbumShareModal,
BackIcon,
DownloadIcon,
EditIcon,
DeleteIcon,
PlusIcon,
@ -122,6 +137,15 @@ export default class AlbumTopMatter extends Mixins(GlobalMixin, UserConfig) {
back() {
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>

View File

@ -38,6 +38,10 @@ export class API {
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() {
return gen(`${BASE}/tags`);
}
@ -91,7 +95,7 @@ export class API {
return tok(gen(`${BASE}/download`));
}
static DOWNLOAD_FILE(handle: number) {
static DOWNLOAD_FILE(handle: string) {
return tok(gen(`${BASE}/download/{handle}`, { handle }));
}

View File

@ -18,7 +18,15 @@ export async function downloadFiles(fileIds: number[]) {
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);
}
/**