diff --git a/CHANGELOG.md b/CHANGELOG.md index b4fc9904..f622a50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/appinfo/routes.php b/appinfo/routes.php index ffb3fde3..83c755d2 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -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'], diff --git a/lib/Controller/AlbumsController.php b/lib/Controller/AlbumsController.php index abd5ce97..4c08ec39 100644 --- a/lib/Controller/AlbumsController.php +++ b/lib/Controller/AlbumsController.php @@ -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); + } } diff --git a/lib/Controller/DownloadController.php b/lib/Controller/DownloadController.php index 9dc21838..24b1d7de 100644 --- a/lib/Controller/DownloadController.php +++ b/lib/Controller/DownloadController.php @@ -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 diff --git a/lib/Db/TimelineQueryAlbums.php b/lib/Db/TimelineQueryAlbums.php index f901ed90..7baa22e6 100644 --- a/lib/Db/TimelineQueryAlbums.php +++ b/lib/Db/TimelineQueryAlbums.php @@ -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() { diff --git a/src/components/top-matter/AlbumTopMatter.vue b/src/components/top-matter/AlbumTopMatter.vue index cc62dc39..bc4e6a38 100644 --- a/src/components/top-matter/AlbumTopMatter.vue +++ b/src/components/top-matter/AlbumTopMatter.vue @@ -29,6 +29,15 @@ {{ t("memories", "Share album") }} + + {{ t("memories", "Download album") }} + + diff --git a/src/services/API.ts b/src/services/API.ts index 3af7d8fb..4b658df1 100644 --- a/src/services/API.ts +++ b/src/services/API.ts @@ -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 })); } diff --git a/src/services/dav/download.ts b/src/services/dav/download.ts index 727076e2..bd2df9af 100644 --- a/src/services/dav/download.ts +++ b/src/services/dav/download.ts @@ -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); } /**