Merge branch 'master' into stable24
commit
abb8dcfc90
|
@ -2,6 +2,15 @@
|
|||
|
||||
This file is manually updated. Please file an issue if something is missing.
|
||||
|
||||
## v4.8.0, v3.8.0
|
||||
|
||||
- **Feature**: Timeline path now scans recursively for mounted volumes / shares inside it
|
||||
- **Feature**: Multiple timeline paths can be specified ([#178](https://github.com/pulsejet/memories/issues/178))
|
||||
- Support for server-side encrypted storage ([#99](https://github.com/pulsejet/memories/issues/99))
|
||||
- Mouse wheel now zooms on desktop
|
||||
- Improved caching performance
|
||||
- Due to incorrect caching in previous versions, your browser cache may have become very large. You can clear it to save some space.
|
||||
|
||||
## v4.7.0, v3.7.0 (2022-11-14)
|
||||
|
||||
- **Note**: you must run `occ memories:index -f` to take advantage of new features.
|
||||
|
|
|
@ -39,7 +39,7 @@ Memories is a _batteries-included_ photo management solution for Nextcloud with
|
|||
|
||||
1. ☁ Clone this into your `apps` folder of your Nextcloud.
|
||||
1. 👩💻 In a terminal, run the command `make dev-setup` to install the dependencies.
|
||||
1. 🏗 To build the Typescript, run `make build-js`. Watch changes with: `make watch-js`.
|
||||
1. 🏗 To build the Typescript, run `make build-js`. Watch changes with: `make watch-js`. Lint-fix PHP with `make php-lint`.
|
||||
1. ✅ Enable the app through the app management of your Nextcloud.
|
||||
1. ⚒️ (Strongly recommended) use VS Code and install Vetur and Prettier.
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ use OCP\IUserManager;
|
|||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleSectionOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Index extends Command
|
||||
|
@ -58,12 +59,14 @@ class Index extends Command
|
|||
protected TimelineWrite $timelineWrite;
|
||||
|
||||
// Stats
|
||||
private int $nUser = 0;
|
||||
private int $nProcessed = 0;
|
||||
private int $nSkipped = 0;
|
||||
private int $nInvalid = 0;
|
||||
private int $nNoMedia = 0;
|
||||
|
||||
// Helper for the progress bar
|
||||
private int $previousLineLength = 0;
|
||||
private ConsoleSectionOutput $outputSection;
|
||||
|
||||
public function __construct(
|
||||
IRootFolder $rootFolder,
|
||||
|
@ -178,8 +181,9 @@ class Index extends Command
|
|||
// Time measurement
|
||||
$startTime = microtime(true);
|
||||
|
||||
if ($this->encryptionManager->isEnabled()) {
|
||||
error_log('FATAL: Encryption is enabled. Aborted.');
|
||||
if (\OCA\Memories\Util::isEncryptionEnabled($this->encryptionManager)) {
|
||||
// Can work with server-side but not with e2e encryption, see https://github.com/pulsejet/memories/issues/99
|
||||
error_log('FATAL: Only server-side encryption (OC_DEFAULT_MODULE) is supported, but another encryption module is enabled. Aborted.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
@ -192,10 +196,11 @@ class Index extends Command
|
|||
// Show some stats
|
||||
$endTime = microtime(true);
|
||||
$execTime = (int) (($endTime - $startTime) * 1000) / 1000;
|
||||
$nTotal = $this->nInvalid + $this->nSkipped + $this->nProcessed;
|
||||
$nTotal = $this->nInvalid + $this->nSkipped + $this->nProcessed + $this->nNoMedia;
|
||||
$this->output->writeln('==========================================');
|
||||
$this->output->writeln("Checked {$nTotal} files in {$execTime} sec");
|
||||
$this->output->writeln("Checked {$nTotal} files of {$this->nUser} users in {$execTime} sec");
|
||||
$this->output->writeln($this->nInvalid.' not valid media items');
|
||||
$this->output->writeln($this->nNoMedia.' .nomedia folders ignored');
|
||||
$this->output->writeln($this->nSkipped.' skipped because unmodified');
|
||||
$this->output->writeln($this->nProcessed.' (re-)processed');
|
||||
$this->output->writeln('==========================================');
|
||||
|
@ -245,31 +250,31 @@ class Index extends Command
|
|||
|
||||
$uid = $user->getUID();
|
||||
$userFolder = $this->rootFolder->getUserFolder($uid);
|
||||
$this->parseFolder($userFolder, $refresh);
|
||||
if ($this->previousLineLength) {
|
||||
$this->output->write("\r".str_repeat(' ', $this->previousLineLength)."\r");
|
||||
}
|
||||
$this->outputSection = $this->output->section();
|
||||
$this->parseFolder($userFolder, $refresh, $this->nUser, $this->userManager->countSeenUsers());
|
||||
$this->outputSection->overwrite('Scanned '.$userFolder->getPath());
|
||||
++$this->nUser;
|
||||
}
|
||||
|
||||
private function parseFolder(Folder &$folder, bool &$refresh): void
|
||||
private function parseFolder(Folder &$folder, bool &$refresh, int $progress_i, int $progress_n): void
|
||||
{
|
||||
try {
|
||||
$folderPath = $folder->getPath();
|
||||
|
||||
// Respect the '.nomedia' file. If present don't traverse the folder
|
||||
if ($folder->nodeExists('.nomedia')) {
|
||||
$this->output->writeln('Skipping folder '.$folderPath.' because of .nomedia file');
|
||||
$this->previousLineLength = 0;
|
||||
++$this->nNoMedia;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$nodes = $folder->getDirectoryListing();
|
||||
|
||||
foreach ($nodes as &$node) {
|
||||
foreach ($nodes as $i => &$node) {
|
||||
if ($node instanceof Folder) {
|
||||
$this->parseFolder($node, $refresh);
|
||||
$this->parseFolder($node, $refresh, $progress_i * \count($nodes) + $i, $progress_n * \count($nodes));
|
||||
} elseif ($node instanceof File) {
|
||||
$this->outputSection->overwrite(sprintf('%.2f%%', $progress_i / $progress_n * 100).' scanning '.$node->getPath());
|
||||
$this->parseFile($node, $refresh);
|
||||
}
|
||||
}
|
||||
|
@ -284,14 +289,6 @@ class Index extends Command
|
|||
|
||||
private function parseFile(File &$file, bool &$refresh): void
|
||||
{
|
||||
// Clear previous line and write new one
|
||||
$line = 'Scanning file '.$file->getPath();
|
||||
if ($this->previousLineLength) {
|
||||
$this->output->write("\r".str_repeat(' ', $this->previousLineLength)."\r");
|
||||
}
|
||||
$this->output->write($line."\r");
|
||||
$this->previousLineLength = \strlen($line);
|
||||
|
||||
// Process the file
|
||||
$res = $this->timelineWrite->processFile($file, $refresh);
|
||||
if (2 === $res) {
|
||||
|
|
|
@ -52,7 +52,15 @@ class VideoSetup extends Command
|
|||
{
|
||||
// Preset executables
|
||||
$ffmpegPath = $this->config->getSystemValue('memories.ffmpeg_path', 'ffmpeg');
|
||||
if ('ffmpeg' === $ffmpegPath) {
|
||||
$ffmpegPath = trim(shell_exec('which ffmpeg') ?: 'ffmpeg');
|
||||
$this->config->setSystemValue('memories.ffmpeg_path', $ffmpegPath);
|
||||
}
|
||||
$ffprobePath = $this->config->getSystemValue('memories.ffprobe_path', 'ffprobe');
|
||||
if ('ffprobe' === $ffprobePath) {
|
||||
$ffprobePath = trim(shell_exec('which ffprobe') ?: 'ffprobe');
|
||||
$this->config->setSystemValue('memories.ffprobe_path', $ffprobePath);
|
||||
}
|
||||
|
||||
// Get ffmpeg version
|
||||
$ffmpeg = shell_exec("{$ffmpegPath} -version");
|
||||
|
@ -80,18 +88,22 @@ class VideoSetup extends Command
|
|||
|
||||
// Check go-vod binary
|
||||
$output->writeln('Checking for go-vod binary');
|
||||
$goVodPath = $this->config->getSystemValue('memories.transcoder', false);
|
||||
|
||||
// Detect architecture
|
||||
$arch = \OCA\Memories\Util::getArch();
|
||||
if (false === $goVodPath) {
|
||||
// Detect architecture
|
||||
$arch = \OCA\Memories\Util::getArch();
|
||||
|
||||
if (!$arch) {
|
||||
$output->writeln('<error>Compatible go-vod binary not found</error>');
|
||||
$this->suggestGoVod($output);
|
||||
if (!$arch) {
|
||||
$output->writeln('<error>Compatible go-vod binary not found</error>');
|
||||
$this->suggestGoVod($output);
|
||||
|
||||
return $this->suggestDisable($output);
|
||||
return $this->suggestDisable($output);
|
||||
}
|
||||
|
||||
$goVodPath = realpath(__DIR__."/../../exiftool-bin/go-vod-{$arch}");
|
||||
}
|
||||
|
||||
$goVodPath = realpath(__DIR__."/../../exiftool-bin/go-vod-{$arch}");
|
||||
$output->writeln("Trying go-vod from {$goVodPath}");
|
||||
chmod($goVodPath, 0755);
|
||||
|
||||
|
|
|
@ -25,12 +25,14 @@ namespace OCA\Memories\Controller;
|
|||
|
||||
use OCA\Memories\AppInfo\Application;
|
||||
use OCA\Memories\Db\TimelineQuery;
|
||||
use OCA\Memories\Db\TimelineRoot;
|
||||
use OCA\Memories\Db\TimelineWrite;
|
||||
use OCA\Memories\Exif;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\Encryption\IManager;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
|
@ -47,6 +49,7 @@ class ApiBase extends Controller
|
|||
protected IUserSession $userSession;
|
||||
protected IRootFolder $rootFolder;
|
||||
protected IAppManager $appManager;
|
||||
protected IManager $encryptionManager;
|
||||
protected TimelineQuery $timelineQuery;
|
||||
protected TimelineWrite $timelineWrite;
|
||||
protected IShareManager $shareManager;
|
||||
|
@ -59,6 +62,7 @@ class ApiBase extends Controller
|
|||
IDBConnection $connection,
|
||||
IRootFolder $rootFolder,
|
||||
IAppManager $appManager,
|
||||
IManager $encryptionManager,
|
||||
IShareManager $shareManager,
|
||||
IPreview $preview
|
||||
) {
|
||||
|
@ -69,6 +73,7 @@ class ApiBase extends Controller
|
|||
$this->connection = $connection;
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->appManager = $appManager;
|
||||
$this->encryptionManager = $encryptionManager;
|
||||
$this->shareManager = $shareManager;
|
||||
$this->previewManager = $preview;
|
||||
$this->timelineQuery = new TimelineQuery($connection);
|
||||
|
@ -88,12 +93,14 @@ class ApiBase extends Controller
|
|||
return $user ? $user->getUID() : '';
|
||||
}
|
||||
|
||||
/** Get the Folder object relevant to the request */
|
||||
protected function getRequestFolder()
|
||||
/** Get the TimelineRoot object relevant to the request */
|
||||
protected function getRequestRoot()
|
||||
{
|
||||
$root = new TimelineRoot();
|
||||
|
||||
// Albums have no folder
|
||||
if ($this->request->getParam('album')) {
|
||||
return null;
|
||||
return $root;
|
||||
}
|
||||
|
||||
// Public shared folder
|
||||
|
@ -103,35 +110,45 @@ class ApiBase extends Controller
|
|||
throw new \Exception('Share not found or invalid');
|
||||
}
|
||||
|
||||
return $share;
|
||||
$root->addFolder($share);
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
// Anything else needs a user
|
||||
$user = $this->userSession->getUser();
|
||||
if (null === $user) {
|
||||
return null;
|
||||
throw new \Exception('User not logged in');
|
||||
}
|
||||
$uid = $user->getUID();
|
||||
|
||||
$folder = null;
|
||||
$folderPath = $this->request->getParam('folder');
|
||||
$forcedTimelinePath = $this->request->getParam('timelinePath');
|
||||
$userFolder = $this->rootFolder->getUserFolder($uid);
|
||||
|
||||
if (null !== $folderPath) {
|
||||
$folder = $userFolder->get($folderPath);
|
||||
} elseif (null !== $forcedTimelinePath) {
|
||||
$folder = $userFolder->get($forcedTimelinePath);
|
||||
} else {
|
||||
$configPath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid));
|
||||
$folder = $userFolder->get($configPath);
|
||||
try {
|
||||
if (null !== $folderPath) {
|
||||
$folder = $userFolder->get(Exif::removeExtraSlash($folderPath));
|
||||
$root->addFolder($folder);
|
||||
} else {
|
||||
$timelinePath = $this->request->getParam('timelinePath', Exif::getPhotosPath($this->config, $uid));
|
||||
$timelinePath = Exif::removeExtraSlash($timelinePath);
|
||||
|
||||
// Multiple timeline path support
|
||||
$paths = explode(';', $timelinePath);
|
||||
foreach ($paths as &$path) {
|
||||
$folder = $userFolder->get(trim($path));
|
||||
$root->addFolder($folder);
|
||||
}
|
||||
$root->addMountPoints();
|
||||
}
|
||||
} catch (\OCP\Files\NotFoundException $e) {
|
||||
$msg = $e->getMessage();
|
||||
|
||||
throw new \Exception("Folder not found: {$msg}");
|
||||
}
|
||||
|
||||
if (!$folder instanceof Folder) {
|
||||
throw new \Exception('Folder not found');
|
||||
}
|
||||
|
||||
return $folder;
|
||||
return $root;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,6 +177,11 @@ class ApiBase extends Controller
|
|||
return null;
|
||||
}
|
||||
|
||||
// Check read permission
|
||||
if (!($file[0]->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $file[0];
|
||||
}
|
||||
|
||||
|
|
|
@ -54,62 +54,94 @@ class ArchiveController extends ApiBase
|
|||
$file = $file[0];
|
||||
|
||||
// Check if user has permissions
|
||||
if (!$file->isUpdateable()) {
|
||||
if (!$file->isUpdateable() || !($file->getPermissions() & \OCP\Constants::PERMISSION_UPDATE)) {
|
||||
return new JSONResponse(['message' => 'Cannot update this file'], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
// Create archive folder in the root of the user's configured timeline
|
||||
$timelinePath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid));
|
||||
$timelineFolder = $userFolder->get($timelinePath);
|
||||
if (null === $timelineFolder || !$timelineFolder instanceof Folder) {
|
||||
return new JSONResponse(['message' => 'Cannot get timeline'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
if (!$timelineFolder->isCreatable()) {
|
||||
return new JSONResponse(['message' => 'Cannot create archive folder'], Http::STATUS_FORBIDDEN);
|
||||
$configPath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid));
|
||||
$configPaths = explode(';', $configPath);
|
||||
$timelineFolders = [];
|
||||
$timelinePaths = [];
|
||||
|
||||
// Get all timeline paths
|
||||
foreach ($configPaths as $path) {
|
||||
try {
|
||||
$f = $userFolder->get($path);
|
||||
$timelineFolders[] = $f;
|
||||
$timelinePaths[] = $f->getPath();
|
||||
} catch (\OCP\Files\NotFoundException $e) {
|
||||
return new JSONResponse(['message' => 'Timeline folder not found'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
// Get path of current file relative to the timeline folder
|
||||
// remove timelineFolder path from start of file path
|
||||
$timelinePath = $timelineFolder->getPath(); // no trailing slash
|
||||
if (substr($file->getPath(), 0, \strlen($timelinePath)) !== $timelinePath) {
|
||||
return new JSONResponse(['message' => 'Files outside timeline cannot be archived'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
$relativePath = substr($file->getPath(), \strlen($timelinePath)); // has a leading slash
|
||||
// Bubble up from file until we reach the correct folder
|
||||
$fileStorageId = $file->getStorage()->getId();
|
||||
$parent = $file->getParent();
|
||||
$isArchived = false;
|
||||
while (true) {
|
||||
if (null === $parent) {
|
||||
throw new \Exception('Cannot get correct parent of file');
|
||||
}
|
||||
|
||||
// Final path of the file including the file name
|
||||
$destinationPath = '';
|
||||
// Hit a timeline folder
|
||||
if (\in_array($parent->getPath(), $timelinePaths, true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Hit a storage root
|
||||
try {
|
||||
if ($parent->getParent()->getStorage()->getId() !== $fileStorageId) {
|
||||
break;
|
||||
}
|
||||
} catch (\OCP\Files\NotFoundException $e) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Hit an archive folder root
|
||||
if ($parent->getName() === \OCA\Memories\Util::$ARCHIVE_FOLDER) {
|
||||
$isArchived = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$parent = $parent->getParent();
|
||||
}
|
||||
|
||||
// Get path of current file relative to the parent folder
|
||||
$relativeFilePath = $parent->getRelativePath($file->getPath());
|
||||
|
||||
// Check if we want to archive or unarchive
|
||||
$body = $this->request->getParams();
|
||||
$unarchive = isset($body['archive']) && false === $body['archive'];
|
||||
if ($isArchived && !$unarchive) {
|
||||
return new JSONResponse(['message' => 'File already archived'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
if (!$isArchived && $unarchive) {
|
||||
return new JSONResponse(['message' => 'File not archived'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Final path of the file including the file name
|
||||
$destinationPath = '';
|
||||
|
||||
// Get if the file is already in the archive (relativePath starts with archive)
|
||||
$archiveFolderWithLeadingSlash = '/'.\OCA\Memories\Util::$ARCHIVE_FOLDER;
|
||||
if (substr($relativePath, 0, \strlen($archiveFolderWithLeadingSlash)) === $archiveFolderWithLeadingSlash) {
|
||||
// file already in archive, remove it instead
|
||||
$destinationPath = substr($relativePath, \strlen($archiveFolderWithLeadingSlash));
|
||||
if (!$unarchive) {
|
||||
return new JSONResponse(['message' => 'File already archived'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
if ($isArchived) {
|
||||
// file already in archive, remove it
|
||||
$destinationPath = $relativeFilePath;
|
||||
$parent = $parent->getParent();
|
||||
} else {
|
||||
// file not in archive, put it in there
|
||||
$destinationPath = Exif::removeExtraSlash(\OCA\Memories\Util::$ARCHIVE_FOLDER.$relativePath);
|
||||
if ($unarchive) {
|
||||
return new JSONResponse(['message' => 'File not archived'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
$af = \OCA\Memories\Util::$ARCHIVE_FOLDER;
|
||||
$destinationPath = Exif::removeExtraSlash($af.$relativeFilePath);
|
||||
}
|
||||
|
||||
// Remove the filename
|
||||
$destinationFolders = explode('/', $destinationPath);
|
||||
$destinationFolders = array_filter(explode('/', $destinationPath));
|
||||
array_pop($destinationFolders);
|
||||
|
||||
// Create folder tree
|
||||
$folder = $timelineFolder;
|
||||
$folder = $parent;
|
||||
foreach ($destinationFolders as $folderName) {
|
||||
if ('' === $folderName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$existingFolder = $folder->get($folderName.'/');
|
||||
if (!$existingFolder instanceof Folder) {
|
||||
|
|
|
@ -23,9 +23,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Memories\Controller;
|
||||
|
||||
use OCA\Memories\Db\TimelineRoot;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\Files\Folder;
|
||||
|
||||
class DaysController extends ApiBase
|
||||
{
|
||||
|
@ -42,10 +42,10 @@ class DaysController extends ApiBase
|
|||
$uid = $this->getUid();
|
||||
|
||||
// Get the folder to show
|
||||
$folder = null;
|
||||
$root = null;
|
||||
|
||||
try {
|
||||
$folder = $this->getRequestFolder();
|
||||
$root = $this->getRequestRoot();
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class DaysController extends ApiBase
|
|||
// Run actual query
|
||||
try {
|
||||
$list = $this->timelineQuery->getDays(
|
||||
$folder,
|
||||
$root,
|
||||
$uid,
|
||||
$this->isRecursive(),
|
||||
$this->isArchive(),
|
||||
|
@ -65,7 +65,7 @@ class DaysController extends ApiBase
|
|||
$list = $this->timelineQuery->daysToMonths($list);
|
||||
} else {
|
||||
// Preload some day responses
|
||||
$this->preloadDays($list, $uid, $folder);
|
||||
$this->preloadDays($list, $uid, $root);
|
||||
}
|
||||
|
||||
// Reverse response if requested. Folders still stay at top.
|
||||
|
@ -75,7 +75,7 @@ class DaysController extends ApiBase
|
|||
|
||||
// Add subfolder info if querying non-recursively
|
||||
if (!$this->isRecursive()) {
|
||||
array_unshift($list, $this->getSubfoldersEntry($folder));
|
||||
array_unshift($list, $this->getSubfoldersEntry($root->getFolder($root->getOneId())));
|
||||
}
|
||||
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
|
@ -111,10 +111,10 @@ class DaysController extends ApiBase
|
|||
}
|
||||
|
||||
// Get the folder to show
|
||||
$folder = null;
|
||||
$root = null;
|
||||
|
||||
try {
|
||||
$folder = $this->getRequestFolder();
|
||||
$root = $this->getRequestRoot();
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ class DaysController extends ApiBase
|
|||
// Run actual query
|
||||
try {
|
||||
$list = $this->timelineQuery->getDay(
|
||||
$folder,
|
||||
$root,
|
||||
$uid,
|
||||
$dayIds,
|
||||
$this->isRecursive(),
|
||||
|
@ -237,11 +237,11 @@ class DaysController extends ApiBase
|
|||
/**
|
||||
* Preload a few "day" at the start of "days" response.
|
||||
*
|
||||
* @param array $days the days array
|
||||
* @param string $uid User ID or blank for public shares
|
||||
* @param null|Folder $folder the folder to search in
|
||||
* @param array $days the days array
|
||||
* @param string $uid User ID or blank for public shares
|
||||
* @param TimelineRoot $root the root folder
|
||||
*/
|
||||
private function preloadDays(array &$days, string $uid, &$folder)
|
||||
private function preloadDays(array &$days, string $uid, TimelineRoot &$root)
|
||||
{
|
||||
$transforms = $this->getTransformations(false);
|
||||
$preloaded = 0;
|
||||
|
@ -263,7 +263,7 @@ class DaysController extends ApiBase
|
|||
|
||||
if (\count($preloadDayIds) > 0) {
|
||||
$allDetails = $this->timelineQuery->getDay(
|
||||
$folder,
|
||||
$root,
|
||||
$uid,
|
||||
$preloadDayIds,
|
||||
$this->isRecursive(),
|
||||
|
|
|
@ -49,14 +49,14 @@ class FacesController extends ApiBase
|
|||
}
|
||||
|
||||
// If this isn't the timeline folder then things aren't going to work
|
||||
$folder = $this->getRequestFolder();
|
||||
if (null === $folder) {
|
||||
$root = $this->getRequestRoot();
|
||||
if ($root->isEmpty()) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Run actual query
|
||||
$list = $this->timelineQuery->getFaces(
|
||||
$folder,
|
||||
$root,
|
||||
);
|
||||
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
|
@ -84,26 +84,32 @@ class FacesController extends ApiBase
|
|||
}
|
||||
|
||||
// Get folder to search for
|
||||
$folder = $this->getRequestFolder();
|
||||
if (null === $folder) {
|
||||
$root = $this->getRequestRoot();
|
||||
if ($root->isEmpty()) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Run actual query
|
||||
$detections = $this->timelineQuery->getFacePreviewDetection($folder, (int) $id);
|
||||
$detections = $this->timelineQuery->getFacePreviewDetection($root, (int) $id);
|
||||
if (null === $detections || 0 === \count($detections)) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Find the first detection that has a preview
|
||||
$preview = null;
|
||||
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
||||
foreach ($detections as &$detection) {
|
||||
// Get the file (also checks permissions)
|
||||
$files = $folder->getById($detection['file_id']);
|
||||
$files = $userFolder->getById($detection['file_id']);
|
||||
if (0 === \count($files) || FileInfo::TYPE_FILE !== $files[0]->getType()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check read permission
|
||||
if (!($files[0]->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get (hopefully cached) preview image
|
||||
try {
|
||||
$preview = $this->previewManager->getPreview($files[0], 2048, 2048, false);
|
||||
|
|
|
@ -13,7 +13,7 @@ trait FoldersTrait
|
|||
/**
|
||||
* Get subfolders entry for days response.
|
||||
*/
|
||||
public function getSubfoldersEntry(Folder &$folder)
|
||||
public function getSubfoldersEntry(Folder $folder)
|
||||
{
|
||||
// Ugly: get the view of the folder with reflection
|
||||
// This is unfortunately the only way to get the contents of a folder
|
||||
|
@ -34,7 +34,7 @@ trait FoldersTrait
|
|||
return [
|
||||
'dayid' => \OCA\Memories\Util::$TAG_DAYID_FOLDERS,
|
||||
'count' => \count($folders),
|
||||
'detail' => array_map(function (&$node) use (&$folder) {
|
||||
'detail' => array_map(function ($node) use (&$folder) {
|
||||
return [
|
||||
'fileid' => $node->getId(),
|
||||
'name' => $node->getName(),
|
||||
|
|
|
@ -71,10 +71,15 @@ class ImageController extends ApiBase
|
|||
}
|
||||
|
||||
// Check if user has permissions
|
||||
if (!$file->isUpdateable()) {
|
||||
if (!$file->isUpdateable() || !($file->getPermissions() & \OCP\Constants::PERMISSION_UPDATE)) {
|
||||
return new JSONResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
// Check for end-to-end encryption
|
||||
if (\OCA\Memories\Util::isEncryptionEnabled($this->encryptionManager)) {
|
||||
return new JSONResponse(['message' => 'Cannot change encrypted file'], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
// Get original file from body
|
||||
$exif = $this->request->getParam('raw');
|
||||
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
|
||||
|
|
|
@ -46,14 +46,14 @@ class TagsController extends ApiBase
|
|||
}
|
||||
|
||||
// If this isn't the timeline folder then things aren't going to work
|
||||
$folder = $this->getRequestFolder();
|
||||
if (null === $folder) {
|
||||
$root = $this->getRequestRoot();
|
||||
if ($root->isEmpty()) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Run actual query
|
||||
$list = $this->timelineQuery->getTags(
|
||||
$folder,
|
||||
$root,
|
||||
);
|
||||
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
|
@ -77,8 +77,8 @@ class TagsController extends ApiBase
|
|||
}
|
||||
|
||||
// If this isn't the timeline folder then things aren't going to work
|
||||
$folder = $this->getRequestFolder();
|
||||
if (null === $folder) {
|
||||
$root = $this->getRequestRoot();
|
||||
if ($root->isEmpty()) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ class TagsController extends ApiBase
|
|||
// Run actual query
|
||||
$list = $this->timelineQuery->getTagPreviews(
|
||||
$tagName,
|
||||
$folder,
|
||||
$root,
|
||||
);
|
||||
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
|
|
|
@ -62,6 +62,10 @@ class VideoController extends ApiBase
|
|||
}
|
||||
$file = $files[0];
|
||||
|
||||
if (!($file->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
|
||||
return new JSONResponse(['message' => 'File not readable'], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
// Local files only for now
|
||||
if (!$file->getStorage()->isLocal()) {
|
||||
return new JSONResponse(['message' => 'External storage not supported'], Http::STATUS_FORBIDDEN);
|
||||
|
|
|
@ -5,27 +5,77 @@ declare(strict_types=1);
|
|||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
const CTE_FOLDERS = // CTE to get all folders recursively in the given top folder
|
||||
'WITH RECURSIVE *PREFIX*cte_folders(fileid) AS (
|
||||
const CTE_FOLDERS = // CTE to get all folders recursively in the given top folders excluding archive
|
||||
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, rootid) AS (
|
||||
SELECT
|
||||
f.fileid
|
||||
f.fileid,
|
||||
f.fileid AS rootid
|
||||
FROM
|
||||
*PREFIX*filecache f
|
||||
WHERE
|
||||
f.fileid = :topFolderId
|
||||
f.fileid IN (:topFolderIds)
|
||||
UNION ALL
|
||||
SELECT
|
||||
f.fileid
|
||||
f.fileid,
|
||||
c.rootid
|
||||
FROM
|
||||
*PREFIX*filecache f
|
||||
INNER JOIN *PREFIX*cte_folders_all c
|
||||
ON (f.parent = c.fileid
|
||||
AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = \'httpd/unix-directory\')
|
||||
AND f.name <> \'.archive\'
|
||||
)
|
||||
), *PREFIX*cte_folders AS (
|
||||
SELECT
|
||||
fileid,
|
||||
MIN(rootid) AS rootid
|
||||
FROM
|
||||
*PREFIX*cte_folders_all
|
||||
GROUP BY
|
||||
fileid
|
||||
)';
|
||||
|
||||
const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the given top folders
|
||||
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, name, rootid) AS (
|
||||
SELECT
|
||||
f.fileid,
|
||||
f.name,
|
||||
f.fileid AS rootid
|
||||
FROM
|
||||
*PREFIX*filecache f
|
||||
WHERE
|
||||
f.fileid IN (:topFolderIds)
|
||||
UNION ALL
|
||||
SELECT
|
||||
f.fileid,
|
||||
f.name,
|
||||
c.rootid
|
||||
FROM
|
||||
*PREFIX*filecache f
|
||||
INNER JOIN *PREFIX*cte_folders_all c
|
||||
ON (f.parent = c.fileid
|
||||
AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = \'httpd/unix-directory\')
|
||||
)
|
||||
), *PREFIX*cte_folders(fileid, rootid) AS (
|
||||
SELECT
|
||||
cfa.fileid,
|
||||
MIN(cfa.rootid) AS rootid
|
||||
FROM
|
||||
*PREFIX*cte_folders_all cfa
|
||||
WHERE
|
||||
cfa.name = \'.archive\'
|
||||
GROUP BY
|
||||
cfa.fileid
|
||||
UNION ALL
|
||||
SELECT
|
||||
f.fileid,
|
||||
c.rootid
|
||||
FROM
|
||||
*PREFIX*filecache f
|
||||
INNER JOIN *PREFIX*cte_folders c
|
||||
ON (f.parent = c.fileid
|
||||
AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = \'httpd/unix-directory\')
|
||||
AND f.fileid <> :excludedFolderId
|
||||
)
|
||||
ON (f.parent = c.fileid)
|
||||
)';
|
||||
|
||||
trait TimelineQueryDays
|
||||
|
@ -35,15 +85,15 @@ trait TimelineQueryDays
|
|||
/**
|
||||
* Get the days response from the database for the timeline.
|
||||
*
|
||||
* @param null|Folder $folder The folder to get the days from
|
||||
* @param bool $recursive Whether to get the days recursively
|
||||
* @param bool $archive Whether to get the days only from the archive folder
|
||||
* @param array $queryTransforms An array of query transforms to apply to the query
|
||||
* @param TimelineRoot $root The root to get the days from
|
||||
* @param bool $recursive Whether to get the days recursively
|
||||
* @param bool $archive Whether to get the days only from the archive folder
|
||||
* @param array $queryTransforms An array of query transforms to apply to the query
|
||||
*
|
||||
* @return array The days response
|
||||
*/
|
||||
public function getDays(
|
||||
&$folder,
|
||||
TimelineRoot &$root,
|
||||
string $uid,
|
||||
bool $recursive,
|
||||
bool $archive,
|
||||
|
@ -56,7 +106,7 @@ trait TimelineQueryDays
|
|||
$query->select('m.dayid', $count)
|
||||
->from('memories', 'm')
|
||||
;
|
||||
$query = $this->joinFilecache($query, $folder, $recursive, $archive);
|
||||
$query = $this->joinFilecache($query, $root, $recursive, $archive);
|
||||
|
||||
// Group and sort by dayid
|
||||
$query->groupBy('m.dayid')
|
||||
|
@ -76,18 +126,18 @@ trait TimelineQueryDays
|
|||
/**
|
||||
* Get the day response from the database for the timeline.
|
||||
*
|
||||
* @param null|Folder $folder The folder to get the day from
|
||||
* @param string $uid The user id
|
||||
* @param int[] $day_ids The day ids to fetch
|
||||
* @param bool $recursive If the query should be recursive
|
||||
* @param bool $archive If the query should include only the archive folder
|
||||
* @param array $queryTransforms The query transformations to apply
|
||||
* @param mixed $day_ids
|
||||
* @param TimelineRoot $root The root to get the day from
|
||||
* @param string $uid The user id
|
||||
* @param int[] $day_ids The day ids to fetch
|
||||
* @param bool $recursive If the query should be recursive
|
||||
* @param bool $archive If the query should include only the archive folder
|
||||
* @param array $queryTransforms The query transformations to apply
|
||||
* @param mixed $day_ids
|
||||
*
|
||||
* @return array An array of day responses
|
||||
*/
|
||||
public function getDay(
|
||||
&$folder,
|
||||
TimelineRoot &$root,
|
||||
string $uid,
|
||||
$day_ids,
|
||||
bool $recursive,
|
||||
|
@ -107,9 +157,14 @@ trait TimelineQueryDays
|
|||
;
|
||||
|
||||
// JOIN with filecache for existing files
|
||||
$query = $this->joinFilecache($query, $folder, $recursive, $archive);
|
||||
$query = $this->joinFilecache($query, $root, $recursive, $archive);
|
||||
$query->addSelect('f.etag', 'f.path', 'f.name AS basename');
|
||||
|
||||
// SELECT rootid if not a single folder
|
||||
if ($recursive && !$root->isEmpty()) {
|
||||
$query->addSelect('cte_f.rootid');
|
||||
}
|
||||
|
||||
// JOIN with mimetypes to get the mimetype
|
||||
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
|
||||
$query->addSelect('mimetypes.mimetype');
|
||||
|
@ -136,7 +191,7 @@ trait TimelineQueryDays
|
|||
$rows = $cursor->fetchAll();
|
||||
$cursor->closeCursor();
|
||||
|
||||
return $this->processDay($rows, $uid, $folder);
|
||||
return $this->processDay($rows, $uid, $root);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -156,37 +211,55 @@ trait TimelineQueryDays
|
|||
|
||||
/**
|
||||
* Process the single day response.
|
||||
*
|
||||
* @param array $day
|
||||
* @param string $uid User or blank if not logged in
|
||||
* @param null|Folder $folder
|
||||
*/
|
||||
private function processDay(&$day, $uid, $folder)
|
||||
private function processDay(array &$day, string $uid, TimelineRoot &$root)
|
||||
{
|
||||
$basePath = '#__#__#';
|
||||
$davPath = '';
|
||||
if (null !== $folder) {
|
||||
/**
|
||||
* Path entry in database for folder.
|
||||
* We need to splice this from the start of the file path.
|
||||
*/
|
||||
$internalPaths = [];
|
||||
|
||||
/**
|
||||
* DAV paths for the folders.
|
||||
* We need to prefix this to the start of the file path.
|
||||
*/
|
||||
$davPaths = [];
|
||||
|
||||
/**
|
||||
* The root folder id for the folder.
|
||||
* We fallback to this if rootid is not found.
|
||||
*/
|
||||
$defaultRootId = 0;
|
||||
|
||||
if (!$root->isEmpty()) {
|
||||
// Get root id of the top folder
|
||||
$defaultRootId = $root->getOneId();
|
||||
|
||||
// No way to get the internal path from the folder
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('path')
|
||||
$query->select('fileid', 'path')
|
||||
->from('filecache')
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($folder->getId(), IQueryBuilder::PARAM_INT)))
|
||||
->where($query->expr()->in('fileid', $query->createNamedParameter($root->getIds(), IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
;
|
||||
$path = $query->executeQuery()->fetchOne();
|
||||
$basePath = $path ?: $basePath;
|
||||
$paths = $query->executeQuery()->fetchAll();
|
||||
foreach ($paths as &$path) {
|
||||
$fileid = (int) $path['fileid'];
|
||||
$internalPaths[$fileid] = $path['path'];
|
||||
|
||||
// Get user facing path
|
||||
// getPath looks like /user/files/... but we want /files/user/...
|
||||
// Split at / and swap these
|
||||
// For public shares, we just give the relative path
|
||||
if (!empty($uid)) {
|
||||
$actualPath = $folder->getPath();
|
||||
$actualPath = explode('/', $actualPath);
|
||||
if (\count($actualPath) >= 3) {
|
||||
$tmp = $actualPath[1];
|
||||
$actualPath[1] = $actualPath[2];
|
||||
$actualPath[2] = $tmp;
|
||||
$davPath = implode('/', $actualPath);
|
||||
// Get DAV path.
|
||||
// getPath looks like /user/files/... but we want /files/user/...
|
||||
// Split at / and swap these
|
||||
// For public shares, we just give the relative path
|
||||
if (!empty($uid) && ($actualPath = $root->getFolderPath($fileid))) {
|
||||
$actualPath = explode('/', $actualPath);
|
||||
if (\count($actualPath) >= 3) {
|
||||
$tmp = $actualPath[1];
|
||||
$actualPath[1] = $actualPath[2];
|
||||
$actualPath[2] = $tmp;
|
||||
$davPath = implode('/', $actualPath);
|
||||
$davPaths[$fileid] = \OCA\Memories\Exif::removeExtraSlash('/'.$davPath.'/');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -212,9 +285,14 @@ trait TimelineQueryDays
|
|||
|
||||
// Check if path exists and starts with basePath and remove
|
||||
if (isset($row['path']) && !empty($row['path'])) {
|
||||
$rootId = \array_key_exists('rootid', $row) ? $row['rootid'] : $defaultRootId;
|
||||
$basePath = $internalPaths[$rootId] ?? '#__#';
|
||||
$davPath = $davPaths[$rootId] ?: '';
|
||||
|
||||
if (0 === strpos($row['path'], $basePath)) {
|
||||
$row['filename'] = $davPath.substr($row['path'], \strlen($basePath));
|
||||
}
|
||||
|
||||
unset($row['path']);
|
||||
}
|
||||
|
||||
|
@ -231,9 +309,14 @@ trait TimelineQueryDays
|
|||
$params = $query->getParameters();
|
||||
$types = $query->getParameterTypes();
|
||||
|
||||
// Get SQL
|
||||
$CTE_SQL = \array_key_exists('cteFoldersArchive', $params) && $params['cteFoldersArchive']
|
||||
? CTE_FOLDERS_ARCHIVE
|
||||
: CTE_FOLDERS;
|
||||
|
||||
// Add WITH clause if needed
|
||||
if (false !== strpos($sql, 'cte_folders')) {
|
||||
$sql = CTE_FOLDERS.' '.$sql;
|
||||
$sql = $CTE_SQL.' '.$sql;
|
||||
}
|
||||
|
||||
return $this->connection->executeQuery($sql, $params, $types);
|
||||
|
@ -244,53 +327,31 @@ trait TimelineQueryDays
|
|||
*/
|
||||
private function addSubfolderJoinParams(
|
||||
IQueryBuilder &$query,
|
||||
Folder &$folder,
|
||||
TimelineRoot &$root,
|
||||
bool $archive
|
||||
) {
|
||||
// Query parameters, set at the end
|
||||
$topFolderId = $folder->getId();
|
||||
$excludedFolderId = -1;
|
||||
|
||||
/** @var Folder Archive folder if it exists */
|
||||
$archiveFolder = null;
|
||||
|
||||
try {
|
||||
$archiveFolder = $folder->get('.archive/');
|
||||
} catch (\OCP\Files\NotFoundException $e) {
|
||||
}
|
||||
|
||||
if (!$archive) {
|
||||
// Exclude archive folder
|
||||
if ($archiveFolder) {
|
||||
$excludedFolderId = $archiveFolder->getId();
|
||||
}
|
||||
} else {
|
||||
// Only include archive folder
|
||||
$topFolderId = $archiveFolder ? $archiveFolder->getId() : -1;
|
||||
}
|
||||
|
||||
// Add query parameters
|
||||
$query->setParameter('topFolderId', $topFolderId, IQueryBuilder::PARAM_INT);
|
||||
$query->setParameter('excludedFolderId', $excludedFolderId, IQueryBuilder::PARAM_INT);
|
||||
$query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY);
|
||||
$query->setParameter('cteFoldersArchive', $archive, IQueryBuilder::PARAM_BOOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner join with oc_filecache.
|
||||
*
|
||||
* @param IQueryBuilder $query Query builder
|
||||
* @param null|Folder $folder Either the top folder or null for all
|
||||
* @param TimelineRoot $root Either the top folder or null for all
|
||||
* @param bool $recursive Whether to get the days recursively
|
||||
* @param bool $archive Whether to get the days only from the archive folder
|
||||
*/
|
||||
private function joinFilecache(
|
||||
IQueryBuilder &$query,
|
||||
&$folder,
|
||||
TimelineRoot &$root,
|
||||
bool $recursive,
|
||||
bool $archive
|
||||
) {
|
||||
// Join with memories
|
||||
$baseOp = $query->expr()->eq('f.fileid', 'm.fileid');
|
||||
if (null === $folder) {
|
||||
if ($root->isEmpty()) {
|
||||
return $query->innerJoin('m', 'filecache', 'f', $baseOp);
|
||||
}
|
||||
|
||||
|
@ -298,11 +359,11 @@ trait TimelineQueryDays
|
|||
$pathOp = null;
|
||||
if ($recursive) {
|
||||
// Join with folders CTE
|
||||
$this->addSubfolderJoinParams($query, $folder, $archive);
|
||||
$this->addSubfolderJoinParams($query, $root, $archive);
|
||||
$query->innerJoin('f', 'cte_folders', 'cte_f', $query->expr()->eq('f.parent', 'cte_f.fileid'));
|
||||
} else {
|
||||
// If getting non-recursively folder only check for parent
|
||||
$pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($folder->getId(), IQueryBuilder::PARAM_INT));
|
||||
$pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($root->getOneId(), IQueryBuilder::PARAM_INT));
|
||||
}
|
||||
|
||||
return $query->innerJoin('m', 'filecache', 'f', $query->expr()->andX(
|
||||
|
|
|
@ -47,7 +47,7 @@ trait TimelineQueryFaces
|
|||
);
|
||||
}
|
||||
|
||||
public function getFaces(Folder $folder)
|
||||
public function getFaces(TimelineRoot &$root)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
|
@ -62,7 +62,7 @@ trait TimelineQueryFaces
|
|||
$query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id'));
|
||||
|
||||
// WHERE these photos are in the user's requested folder recursively
|
||||
$query = $this->joinFilecache($query, $folder, true, false);
|
||||
$query = $this->joinFilecache($query, $root, true, false);
|
||||
|
||||
// GROUP by ID of face cluster
|
||||
$query->groupBy('rfc.id');
|
||||
|
@ -87,7 +87,7 @@ trait TimelineQueryFaces
|
|||
return $faces;
|
||||
}
|
||||
|
||||
public function getFacePreviewDetection(Folder &$folder, int $id)
|
||||
public function getFacePreviewDetection(TimelineRoot &$root, int $id)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
|
@ -109,7 +109,7 @@ trait TimelineQueryFaces
|
|||
$query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id'));
|
||||
|
||||
// WHERE these photos are in the user's requested folder recursively
|
||||
$query = $this->joinFilecache($query, $folder, true, false);
|
||||
$query = $this->joinFilecache($query, $root, true, false);
|
||||
|
||||
// LIMIT results
|
||||
$query->setMaxResults(15);
|
||||
|
|
|
@ -19,7 +19,9 @@ trait TimelineQueryFolders
|
|||
$query->select('f.fileid', 'f.etag')->from('memories', 'm');
|
||||
|
||||
// WHERE these photos are in the user's requested folder recursively
|
||||
$query = $this->joinFilecache($query, $folder, true, false);
|
||||
$root = new TimelineRoot();
|
||||
$root->addFolder($folder);
|
||||
$query = $this->joinFilecache($query, $root, true, false);
|
||||
|
||||
// ORDER descending by fileid
|
||||
$query->orderBy('f.fileid', 'DESC');
|
||||
|
|
|
@ -38,7 +38,7 @@ trait TimelineQueryTags
|
|||
));
|
||||
}
|
||||
|
||||
public function getTags(Folder $folder)
|
||||
public function getTags(TimelineRoot &$root)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
|
@ -58,7 +58,7 @@ trait TimelineQueryTags
|
|||
$query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid'));
|
||||
|
||||
// WHERE these photos are in the user's requested folder recursively
|
||||
$query = $this->joinFilecache($query, $folder, true, false);
|
||||
$query = $this->joinFilecache($query, $root, true, false);
|
||||
|
||||
// GROUP and ORDER by tag name
|
||||
$query->groupBy('st.id');
|
||||
|
@ -78,7 +78,7 @@ trait TimelineQueryTags
|
|||
return $tags;
|
||||
}
|
||||
|
||||
public function getTagPreviews(string $tagName, Folder &$folder)
|
||||
public function getTagPreviews(string $tagName, TimelineRoot &$root)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$tagId = $this->getSystemTagId($query, $tagName);
|
||||
|
@ -99,7 +99,7 @@ trait TimelineQueryTags
|
|||
$query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid'));
|
||||
|
||||
// WHERE these photos are in the user's requested folder recursively
|
||||
$query = $this->joinFilecache($query, $folder, true, false);
|
||||
$query = $this->joinFilecache($query, $root, true, false);
|
||||
|
||||
// MAX 4
|
||||
$query->setMaxResults(4);
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\Node;
|
||||
|
||||
class TimelineRoot
|
||||
{
|
||||
protected array $folders;
|
||||
protected array $folderPaths;
|
||||
|
||||
/** Initialize */
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a folder to the root.
|
||||
*
|
||||
* @param Node $folder Node to add
|
||||
*
|
||||
* @throws \Exception if node is not valid readable folder
|
||||
*/
|
||||
public function addFolder(Node &$folder)
|
||||
{
|
||||
$folderPath = $folder->getPath();
|
||||
|
||||
if (!$folder instanceof Folder) {
|
||||
throw new \Exception("Not a folder: {$folderPath}");
|
||||
}
|
||||
|
||||
if (!($folder->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
|
||||
throw new \Exception("Folder not readable: {$folderPath}");
|
||||
}
|
||||
|
||||
// Add top level folder
|
||||
$id = $folder->getId();
|
||||
$this->folders[$id] = $folder;
|
||||
$this->folderPaths[$id] = $folderPath;
|
||||
}
|
||||
|
||||
// Add mountpoints recursively
|
||||
public function addMountPoints()
|
||||
{
|
||||
$mp = [];
|
||||
foreach ($this->folderPaths as $id => $folderPath) {
|
||||
$mounts = \OC\Files\Filesystem::getMountManager()->findIn($folderPath);
|
||||
foreach ($mounts as &$mount) {
|
||||
$id = $mount->getStorageRootId();
|
||||
$path = $mount->getMountPoint();
|
||||
$mp[$id] = $path;
|
||||
}
|
||||
}
|
||||
$this->folderPaths += $mp;
|
||||
}
|
||||
|
||||
public function getFolderPath(int $id)
|
||||
{
|
||||
return $this->folderPaths[$id];
|
||||
}
|
||||
|
||||
public function getIds()
|
||||
{
|
||||
return array_keys($this->folderPaths);
|
||||
}
|
||||
|
||||
public function getOneId()
|
||||
{
|
||||
return array_key_first($this->folders);
|
||||
}
|
||||
|
||||
public function getFolder(int $id)
|
||||
{
|
||||
return $this->folders[$id];
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
{
|
||||
return empty($this->folderPaths);
|
||||
}
|
||||
}
|
|
@ -114,8 +114,12 @@ class TimelineWrite
|
|||
$exifJson = json_encode($exif);
|
||||
|
||||
// Store error if data > 64kb
|
||||
if (\strlen($exifJson) > 65535) {
|
||||
$exifJson = json_encode(['error' => 'Exif data too large']);
|
||||
if (\is_string($exifJson)) {
|
||||
if (\strlen($exifJson) > 65535) {
|
||||
$exifJson = json_encode(['error' => 'Exif data too large']);
|
||||
}
|
||||
} else {
|
||||
$exifJson = json_encode(['error' => 'Exif data encoding error']);
|
||||
}
|
||||
|
||||
if ($prevRow) {
|
||||
|
|
|
@ -368,7 +368,7 @@ class Exif
|
|||
|
||||
private static function getExifFromLocalPathWithStaticProc(string &$path)
|
||||
{
|
||||
fwrite(self::$staticPipes[0], "{$path}\n-json\n-b\n-api\nQuickTimeUTC=1\n-n\n-execute\n");
|
||||
fwrite(self::$staticPipes[0], "{$path}\n-U\n-json\n--b\n-api\nQuickTimeUTC=1\n-n\n-execute\n");
|
||||
fflush(self::$staticPipes[0]);
|
||||
|
||||
$readyToken = "\n{ready}\n";
|
||||
|
@ -390,7 +390,7 @@ class Exif
|
|||
private static function getExifFromLocalPathWithSeparateProc(string &$path)
|
||||
{
|
||||
$pipes = [];
|
||||
$proc = proc_open(array_merge(self::getExiftool(), ['-api', 'QuickTimeUTC=1', '-n', '-json', '-b', $path]), [
|
||||
$proc = proc_open(array_merge(self::getExiftool(), ['-api', 'QuickTimeUTC=1', '-n', '-U', '-json', '--b', $path]), [
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
], $pipes);
|
||||
|
|
32
lib/Util.php
32
lib/Util.php
|
@ -4,9 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Memories;
|
||||
|
||||
use OCA\Memories\AppInfo\Application;
|
||||
use OCP\IConfig;
|
||||
|
||||
class Util
|
||||
{
|
||||
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
|
||||
|
@ -47,19 +44,6 @@ class Util
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the user's configured photos directory.
|
||||
*/
|
||||
public static function getPhotosPath(IConfig &$config, string $userId)
|
||||
{
|
||||
$p = $config->getUserValue($userId, Application::APPNAME, 'timelinePath', '');
|
||||
if (empty($p)) {
|
||||
return '/Photos/';
|
||||
}
|
||||
|
||||
return $p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if albums are enabled for this user.
|
||||
*
|
||||
|
@ -121,4 +105,20 @@ class Util
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any encryption is enabled that we can not cope with
|
||||
* such as end-to-end encryption.
|
||||
*
|
||||
* @param mixed $encryptionManager
|
||||
*/
|
||||
public static function isEncryptionEnabled(&$encryptionManager): bool
|
||||
{
|
||||
if ($encryptionManager->isEnabled()) {
|
||||
// Server-side encryption (OC_DEFAULT_MODULE) is okay, others like e2e are not
|
||||
return 'OC_DEFAULT_MODULE' !== $encryptionManager->getDefaultEncryptionModuleId();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
"justified-layout": "^4.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"path-posix": "^1.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"plyr": "^3.7.2",
|
||||
"photoswipe": "^5.3.4",
|
||||
"plyr": "^3.7.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"video.js": "^7.20.3",
|
||||
"videojs-contrib-quality-levels": "^2.2.0",
|
||||
|
@ -29,17 +29,17 @@
|
|||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-virtual-scroller": "1.1.2",
|
||||
"webdav": "^4.11.0"
|
||||
"webdav": "^4.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
"@nextcloud/browserslist-config": "^2.3.0",
|
||||
"@nextcloud/webpack-vue-config": "^5.3.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@nextcloud/webpack-vue-config": "^5.4.0",
|
||||
"@playwright/test": "^1.28.0",
|
||||
"@types/url-parse": "^1.4.8",
|
||||
"playwright": "^1.27.1",
|
||||
"playwright": "^1.28.0",
|
||||
"ts-loader": "^9.4.1",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "^4.9.3",
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2023,13 +2023,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
|
||||
"integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.27.1"
|
||||
"playwright-core": "1.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
@ -3755,9 +3755,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.26.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
|
||||
"integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==",
|
||||
"version": "3.26.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz",
|
||||
"integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
@ -5239,9 +5239,9 @@
|
|||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/hot-patcher": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-0.5.0.tgz",
|
||||
"integrity": "sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw=="
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-1.0.0.tgz",
|
||||
"integrity": "sha512-3H8VH0PreeNsKMZw16nTHbUp4YoHCnPlawpsPXGJUR4qENDynl79b6Xk9CIFvLcH1qungBsCuzKcWyzoPPalTw=="
|
||||
},
|
||||
"node_modules/hpack.js": {
|
||||
"version": "2.1.6",
|
||||
|
@ -7037,9 +7037,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/photoswipe": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.3.tgz",
|
||||
"integrity": "sha512-BUuulwZwkYFKADSe5xf0dd+wf6dws34ZvqP8R3oYHepRauOXoQHvw600sw1HlWd8K0S3LRCS4jxyR5fTuI383Q==",
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.4.tgz",
|
||||
"integrity": "sha512-SN+RWHqxJvdwzXJsh8KrG+ajjPpdTX5HpKglEd0k9o6o5fW+QHPkW8//Bo11MB+NQwTa/hFw8BDv2EdxiDXjNw==",
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
|
@ -7085,13 +7085,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.27.1.tgz",
|
||||
"integrity": "sha512-xXYZ7m36yTtC+oFgqH0eTgullGztKSRMb4yuwLPl8IYSmgBM88QiB+3IWb1mRIC9/NNwcgbG0RwtFlg+EAFQHQ==",
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.0.tgz",
|
||||
"integrity": "sha512-kyOXGc5y1mgi+hgEcCIyE1P1+JumLrxS09nFHo5sdJNzrucxPRAGwM4A2X3u3SDOfdgJqx61yIoR6Av+5plJPg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.1"
|
||||
"playwright-core": "1.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
@ -7101,9 +7101,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
|
||||
"integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
@ -7113,11 +7113,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/plyr": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.2.tgz",
|
||||
"integrity": "sha512-I0ZC/OI4oJ0iWG9s2rrnO0YFO6aLyrPiQBq9kum0FqITYljwTPBbYL3TZZu8UJQJUq7tUWN18Q7ACwNCkGKABQ==",
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.3.tgz",
|
||||
"integrity": "sha512-ORULENBvEvvzMYXRQBALDmEi8P+wZt1Hr/NvHqchu/t7E2xJKNkRYWx0qCA1HETIGZ6zobrOVgqeAUqWimS7fQ==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.22.0",
|
||||
"core-js": "^3.26.1",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^4.2.0",
|
||||
"rangetouch": "^2.0.1",
|
||||
|
@ -9074,9 +9074,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
|
||||
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
@ -9659,16 +9659,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/webdav": {
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.0.tgz",
|
||||
"integrity": "sha512-vQ2EFL8cef9F/Nvua1NPcw3z9CWAnnc22mn+sym72W2WFW4Q7doTIhItRzxpgU+tUCc3V10VB0I+eBdgU5wKTQ==",
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.2.tgz",
|
||||
"integrity": "sha512-Ht9TPD5EB7gYW0YmhRcE5NW0/dn/HQfyLSPQY1Rw1coQ5MQTUooAQ9Bpqt4EU7QLw0b95tX4cU59R+SIojs9KQ==",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"base-64": "^1.0.0",
|
||||
"byte-length": "^1.0.2",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"he": "^1.2.0",
|
||||
"hot-patcher": "^0.5.0",
|
||||
"hot-patcher": "^1.0.0",
|
||||
"layerr": "^0.1.2",
|
||||
"md5": "^2.3.0",
|
||||
"minimatch": "^5.1.0",
|
||||
|
@ -11855,13 +11855,13 @@
|
|||
"requires": {}
|
||||
},
|
||||
"@playwright/test": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
|
||||
"integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.27.1"
|
||||
"playwright-core": "1.28.0"
|
||||
}
|
||||
},
|
||||
"@popperjs/core": {
|
||||
|
@ -13288,9 +13288,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.26.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
|
||||
"integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw=="
|
||||
"version": "3.26.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz",
|
||||
"integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.26.0",
|
||||
|
@ -14471,9 +14471,9 @@
|
|||
}
|
||||
},
|
||||
"hot-patcher": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-0.5.0.tgz",
|
||||
"integrity": "sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw=="
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-1.0.0.tgz",
|
||||
"integrity": "sha512-3H8VH0PreeNsKMZw16nTHbUp4YoHCnPlawpsPXGJUR4qENDynl79b6Xk9CIFvLcH1qungBsCuzKcWyzoPPalTw=="
|
||||
},
|
||||
"hpack.js": {
|
||||
"version": "2.1.6",
|
||||
|
@ -15838,9 +15838,9 @@
|
|||
}
|
||||
},
|
||||
"photoswipe": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.3.tgz",
|
||||
"integrity": "sha512-BUuulwZwkYFKADSe5xf0dd+wf6dws34ZvqP8R3oYHepRauOXoQHvw600sw1HlWd8K0S3LRCS4jxyR5fTuI383Q=="
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.4.tgz",
|
||||
"integrity": "sha512-SN+RWHqxJvdwzXJsh8KrG+ajjPpdTX5HpKglEd0k9o6o5fW+QHPkW8//Bo11MB+NQwTa/hFw8BDv2EdxiDXjNw=="
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
|
@ -15871,26 +15871,26 @@
|
|||
}
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.27.1.tgz",
|
||||
"integrity": "sha512-xXYZ7m36yTtC+oFgqH0eTgullGztKSRMb4yuwLPl8IYSmgBM88QiB+3IWb1mRIC9/NNwcgbG0RwtFlg+EAFQHQ==",
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.0.tgz",
|
||||
"integrity": "sha512-kyOXGc5y1mgi+hgEcCIyE1P1+JumLrxS09nFHo5sdJNzrucxPRAGwM4A2X3u3SDOfdgJqx61yIoR6Av+5plJPg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"playwright-core": "1.27.1"
|
||||
"playwright-core": "1.28.0"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
|
||||
"integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true
|
||||
},
|
||||
"plyr": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.2.tgz",
|
||||
"integrity": "sha512-I0ZC/OI4oJ0iWG9s2rrnO0YFO6aLyrPiQBq9kum0FqITYljwTPBbYL3TZZu8UJQJUq7tUWN18Q7ACwNCkGKABQ==",
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.3.tgz",
|
||||
"integrity": "sha512-ORULENBvEvvzMYXRQBALDmEi8P+wZt1Hr/NvHqchu/t7E2xJKNkRYWx0qCA1HETIGZ6zobrOVgqeAUqWimS7fQ==",
|
||||
"requires": {
|
||||
"core-js": "^3.22.0",
|
||||
"core-js": "^3.26.1",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^4.2.0",
|
||||
"rangetouch": "^2.0.1",
|
||||
|
@ -17381,9 +17381,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
|
||||
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||
"dev": true
|
||||
},
|
||||
"unbox-primitive": {
|
||||
|
@ -17837,16 +17837,16 @@
|
|||
}
|
||||
},
|
||||
"webdav": {
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.0.tgz",
|
||||
"integrity": "sha512-vQ2EFL8cef9F/Nvua1NPcw3z9CWAnnc22mn+sym72W2WFW4Q7doTIhItRzxpgU+tUCc3V10VB0I+eBdgU5wKTQ==",
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.2.tgz",
|
||||
"integrity": "sha512-Ht9TPD5EB7gYW0YmhRcE5NW0/dn/HQfyLSPQY1Rw1coQ5MQTUooAQ9Bpqt4EU7QLw0b95tX4cU59R+SIojs9KQ==",
|
||||
"requires": {
|
||||
"axios": "^0.27.2",
|
||||
"base-64": "^1.0.0",
|
||||
"byte-length": "^1.0.2",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"he": "^1.2.0",
|
||||
"hot-patcher": "^0.5.0",
|
||||
"hot-patcher": "^1.0.0",
|
||||
"layerr": "^0.1.2",
|
||||
"md5": "^2.3.0",
|
||||
"minimatch": "^5.1.0",
|
||||
|
|
14
package.json
14
package.json
|
@ -38,8 +38,8 @@
|
|||
"justified-layout": "^4.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"path-posix": "^1.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"plyr": "^3.7.2",
|
||||
"photoswipe": "^5.3.4",
|
||||
"plyr": "^3.7.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"video.js": "^7.20.3",
|
||||
"videojs-contrib-quality-levels": "^2.2.0",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-virtual-scroller": "1.1.2",
|
||||
"webdav": "^4.11.0"
|
||||
"webdav": "^4.11.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
|
@ -61,12 +61,12 @@
|
|||
"devDependencies": {
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
"@nextcloud/browserslist-config": "^2.3.0",
|
||||
"@nextcloud/webpack-vue-config": "^5.3.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@nextcloud/webpack-vue-config": "^5.4.0",
|
||||
"@playwright/test": "^1.28.0",
|
||||
"@types/url-parse": "^1.4.8",
|
||||
"playwright": "^1.27.1",
|
||||
"playwright": "^1.28.0",
|
||||
"ts-loader": "^9.4.1",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "^4.9.3",
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ mv "exiftool-$exifver" exiftool
|
|||
rm -rf *.zip exiftool/t exiftool/html
|
||||
chmod 755 exiftool/exiftool
|
||||
|
||||
govod="0.0.12"
|
||||
govod="0.0.15"
|
||||
wget -q "https://github.com/pulsejet/go-vod/releases/download/$govod/go-vod-amd64"
|
||||
wget -q "https://github.com/pulsejet/go-vod/releases/download/$govod/go-vod-aarch64"
|
||||
chmod 755 go-vod-*
|
||||
|
|
|
@ -325,6 +325,7 @@ body {
|
|||
#app-navigation-vue {
|
||||
border-top-left-radius: var(--body-container-radius);
|
||||
border-bottom-left-radius: var(--body-container-radius);
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -220,6 +220,11 @@ export default class ImageEditor extends Mixins(GlobalMixin) {
|
|||
delete exif.ExifImageHeight;
|
||||
delete exif.ExifImageWidth;
|
||||
delete exif.ExifImageSize;
|
||||
delete exif.CompatibleBrands;
|
||||
delete exif.FileType;
|
||||
delete exif.FileTypeExtension;
|
||||
delete exif.MIMEType;
|
||||
delete exif.MajorBrand;
|
||||
|
||||
// Update exif data
|
||||
await axios.patch(
|
||||
|
|
|
@ -164,6 +164,7 @@ class VideoContentSetup {
|
|||
|
||||
sources.push({
|
||||
src: content.data.src,
|
||||
type: "video/mp4",
|
||||
});
|
||||
|
||||
const overrideNative = !videojs.browser.IS_SAFARI;
|
||||
|
@ -287,7 +288,7 @@ class VideoContentSetup {
|
|||
},
|
||||
fullscreen: {
|
||||
enabled: true,
|
||||
container: ".pswp__item",
|
||||
container: ".pswp__container",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ import AddToAlbumModal from "./modal/AddToAlbumModal.vue";
|
|||
|
||||
import StarIcon from "vue-material-design-icons/Star.vue";
|
||||
import DownloadIcon from "vue-material-design-icons/Download.vue";
|
||||
import DeleteIcon from "vue-material-design-icons/Delete.vue";
|
||||
import DeleteIcon from "vue-material-design-icons/TrashCanOutline.vue";
|
||||
import EditIcon from "vue-material-design-icons/ClockEdit.vue";
|
||||
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
|
||||
import UnarchiveIcon from "vue-material-design-icons/PackageUp.vue";
|
||||
|
|
|
@ -53,6 +53,12 @@
|
|||
>
|
||||
{{ t("memories", "Square grid mode") }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<MultiPathSelectionModal
|
||||
ref="multiPathModal"
|
||||
:title="pathSelTitle"
|
||||
@close="saveTimelinePath"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -65,18 +71,24 @@ input[type="text"] {
|
|||
<script lang="ts">
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
import GlobalMixin from "../mixins/GlobalMixin";
|
||||
|
||||
import { getFilePickerBuilder } from "@nextcloud/dialogs";
|
||||
import UserConfig from "../mixins/UserConfig";
|
||||
|
||||
import { getFilePickerBuilder } from "@nextcloud/dialogs";
|
||||
import { NcCheckboxRadioSwitch } from "@nextcloud/vue";
|
||||
|
||||
import MultiPathSelectionModal from "./modal/MultiPathSelectionModal.vue";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
NcCheckboxRadioSwitch,
|
||||
MultiPathSelectionModal,
|
||||
},
|
||||
})
|
||||
export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
||||
get pathSelTitle() {
|
||||
return this.t("memories", "Choose Timeline Paths");
|
||||
}
|
||||
|
||||
async chooseFolder(title: string, initial: string) {
|
||||
const picker = getFilePickerBuilder(title)
|
||||
.setMultiSelect(false)
|
||||
|
@ -91,10 +103,13 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
|||
}
|
||||
|
||||
async chooseTimelinePath() {
|
||||
const newPath = await this.chooseFolder(
|
||||
this.t("memories", "Choose the root of your timeline"),
|
||||
this.config_timelinePath
|
||||
);
|
||||
(<any>this.$refs.multiPathModal).open(this.config_timelinePath.split(";"));
|
||||
}
|
||||
|
||||
async saveTimelinePath(paths: string[]) {
|
||||
if (!paths || !paths.length) return;
|
||||
|
||||
const newPath = paths.join(";");
|
||||
if (newPath !== this.config_timelinePath) {
|
||||
this.config_timelinePath = newPath;
|
||||
await this.updateSetting("timelinePath");
|
||||
|
@ -102,10 +117,11 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
|||
}
|
||||
|
||||
async chooseFoldersPath() {
|
||||
const newPath = await this.chooseFolder(
|
||||
let newPath = await this.chooseFolder(
|
||||
this.t("memories", "Choose the root for the folders view"),
|
||||
this.config_foldersPath
|
||||
);
|
||||
if (newPath === "") newPath = "/";
|
||||
if (newPath !== this.config_foldersPath) {
|
||||
this.config_foldersPath = newPath;
|
||||
await this.updateSetting("foldersPath");
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
{{ item.super }}
|
||||
</div>
|
||||
<div class="main" @click="selectionManager.selectHead(item)">
|
||||
<CheckCircle :size="18" class="select" v-if="item.name" />
|
||||
<CheckCircle :size="20" class="select" v-if="item.name" />
|
||||
<span class="name"> {{ item.name || getHeadName(item) }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -917,7 +917,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
head.day.detail.length === photos.length &&
|
||||
head.day.detail.every(
|
||||
(p, i) =>
|
||||
p.fileid === photos[i].fileid && p.etag === photos[i].etag
|
||||
p.fileid === photos[i].fileid &&
|
||||
p.etag === photos[i].etag &&
|
||||
p.filename === photos[i].filename
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
|
@ -1316,6 +1318,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
display: block;
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
font-size: 1.075em;
|
||||
}
|
||||
|
||||
:hover,
|
||||
|
@ -1325,7 +1328,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
opacity: 0.7;
|
||||
}
|
||||
.name {
|
||||
transform: translateX(22px);
|
||||
transform: translateX(24px);
|
||||
}
|
||||
}
|
||||
&.selected .select {
|
||||
|
|
|
@ -122,7 +122,7 @@ import "photoswipe/style.css";
|
|||
import PsVideo from "./PsVideo";
|
||||
|
||||
import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
|
||||
import DeleteIcon from "vue-material-design-icons/Delete.vue";
|
||||
import DeleteIcon from "vue-material-design-icons/TrashCanOutline.vue";
|
||||
import StarIcon from "vue-material-design-icons/Star.vue";
|
||||
import StarOutlineIcon from "vue-material-design-icons/StarOutline.vue";
|
||||
import DownloadIcon from "vue-material-design-icons/Download.vue";
|
||||
|
@ -253,9 +253,13 @@ export default class Viewer extends Mixins(GlobalMixin) {
|
|||
counter: true,
|
||||
zoom: false,
|
||||
loop: false,
|
||||
wheelToZoom: true,
|
||||
bgOpacity: 1,
|
||||
appendToEl: this.$refs.inner as HTMLElement,
|
||||
preload: [2, 2],
|
||||
closeTitle: this.t("memories", "Close"),
|
||||
arrowPrevTitle: this.t("memories", "Previous"),
|
||||
arrowNextTitle: this.t("memories", "Next"),
|
||||
getViewportSizeFn: () => {
|
||||
const sidebarWidth = this.sidebarOpen ? this.sidebarWidth : 0;
|
||||
this.outerWidth = `calc(100vw - ${sidebarWidth}px)`;
|
||||
|
|
|
@ -261,7 +261,7 @@ $icon-size: $icon-half-size * 2;
|
|||
// Extremely ugly way to fill up the space
|
||||
// If this isn't done, bg has a border
|
||||
:deep path {
|
||||
transform: scale(1.19) translate(-1.85px, -1.85px);
|
||||
transform: scale(1.2) translate(-2px, -2px);
|
||||
}
|
||||
|
||||
filter: invert(1) brightness(100);
|
||||
|
|
|
@ -335,12 +335,11 @@ export default class EditDate extends Mixins(GlobalMixin) {
|
|||
const offset = date.getTime() - pDate.getTime();
|
||||
const scale = diff > 0 ? diffNew / diff : 0;
|
||||
const pDateNew = new Date(dateNew.getTime() - offset * scale);
|
||||
const res = await axios.patch<any>(
|
||||
generateUrl(EDIT_API_URL, { id: p.fileid }),
|
||||
{
|
||||
date: this.getExifFormat(pDateNew),
|
||||
}
|
||||
);
|
||||
await axios.patch<any>(generateUrl(EDIT_API_URL, { id: p.fileid }), {
|
||||
raw: {
|
||||
DateTimeOriginal: this.getExifFormat(pDateNew),
|
||||
},
|
||||
});
|
||||
emit("files:file:updated", { fileid: p.fileid });
|
||||
} catch (e) {
|
||||
if (e.response?.data?.message) {
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<Modal @close="close" v-if="show" size="small">
|
||||
<template #title>
|
||||
{{ title }}
|
||||
</template>
|
||||
|
||||
<ul>
|
||||
<li v-for="(path, index) in paths" :key="index" class="path">
|
||||
{{ path }}
|
||||
|
||||
<NcActions :inline="1">
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Remove')"
|
||||
@click="remove(index)"
|
||||
>
|
||||
{{ t("memories", "Remove") }}
|
||||
<template #icon> <CloseIcon :size="20" /> </template>
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<template #buttons>
|
||||
<NcButton @click="add" class="button" type="secondary">
|
||||
{{ t("memories", "Add Path") }}
|
||||
</NcButton>
|
||||
<NcButton @click="save" class="button" type="primary">
|
||||
{{ t("memories", "Save") }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Emit, Mixins, Prop } from "vue-property-decorator";
|
||||
import GlobalMixin from "../../mixins/GlobalMixin";
|
||||
import UserConfig from "../../mixins/UserConfig";
|
||||
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
import { getFilePickerBuilder } from "@nextcloud/dialogs";
|
||||
import { NcActions, NcActionButton, NcButton } from "@nextcloud/vue";
|
||||
|
||||
import CloseIcon from "vue-material-design-icons/Close.vue";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Modal,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
NcButton,
|
||||
CloseIcon,
|
||||
},
|
||||
})
|
||||
export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
||||
@Prop({ required: true }) title: string;
|
||||
|
||||
private show = false;
|
||||
private paths: string[] = [];
|
||||
|
||||
@Emit("close")
|
||||
public close(list: string[]) {
|
||||
this.show = false;
|
||||
}
|
||||
|
||||
public open(paths: string[]) {
|
||||
this.paths = paths;
|
||||
this.show = true;
|
||||
}
|
||||
|
||||
public save() {
|
||||
this.close(this.paths);
|
||||
}
|
||||
|
||||
async chooseFolder(title: string, initial: string) {
|
||||
const picker = getFilePickerBuilder(title)
|
||||
.setMultiSelect(false)
|
||||
.setModal(true)
|
||||
.setType(1)
|
||||
.addMimeTypeFilter("httpd/unix-directory")
|
||||
.allowDirectories()
|
||||
.startAt(initial)
|
||||
.build();
|
||||
|
||||
return await picker.pick();
|
||||
}
|
||||
|
||||
public async add() {
|
||||
let newPath = await this.chooseFolder(
|
||||
this.t("memories", "Add a root to your timeline"),
|
||||
"/"
|
||||
);
|
||||
if (newPath === "") newPath = "/";
|
||||
this.paths.push(newPath);
|
||||
}
|
||||
|
||||
public remove(index: number) {
|
||||
this.paths.splice(index, 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.path {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.1rem;
|
||||
padding-left: 10px;
|
||||
word-wrap: break-all;
|
||||
}
|
||||
</style>
|
|
@ -334,7 +334,11 @@ export function cacheData(url: string, data: Object) {
|
|||
if (!cache) return;
|
||||
|
||||
const response = new Response(str);
|
||||
const encoded = new TextEncoder().encode(str);
|
||||
response.headers.set("Content-Type", "application/json");
|
||||
response.headers.set("Content-Length", encoded.length.toString());
|
||||
response.headers.set("Cache-Control", "max-age=604800"); // 1 week
|
||||
response.headers.set("Vary", "Accept-Encoding");
|
||||
await cache.put(url, response);
|
||||
})();
|
||||
}
|
||||
|
|
48
webpack.js
48
webpack.js
|
@ -26,6 +26,18 @@ webpackConfig.watchOptions = {
|
|||
};
|
||||
|
||||
if (!isDev) {
|
||||
const imageCacheOpts = (expiryDays) => ({
|
||||
handler: 'CacheFirst',
|
||||
|
||||
options: {
|
||||
cacheName: 'images',
|
||||
expiration: {
|
||||
maxAgeSeconds: 3600 * 24 * expiryDays, // days
|
||||
maxEntries: 20000, // 20k images
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new WorkboxPlugin.GenerateSW({
|
||||
swDest: 'memories-service-worker.js',
|
||||
|
@ -37,17 +49,33 @@ if (!isDev) {
|
|||
|
||||
// Define runtime caching rules.
|
||||
runtimeCaching: [{
|
||||
// Match any preview file request
|
||||
// Do not cache video related files
|
||||
urlPattern: /^.*\/apps\/memories\/api\/video\/.*/,
|
||||
handler: 'NetworkOnly',
|
||||
}, {
|
||||
// Do not cache raw editing files
|
||||
urlPattern: /^.*\/apps\/memories\/api\/image\/jpeg\/.*/,
|
||||
handler: 'NetworkOnly',
|
||||
}, {
|
||||
// Do not cache webdav
|
||||
urlPattern: /^.*\/remote.php\/.*/,
|
||||
handler: 'NetworkOnly',
|
||||
}, {
|
||||
// Do not cache downloads
|
||||
urlPattern: /^.*\/apps\/files\/ajax\/download.php?.*/,
|
||||
handler: 'NetworkOnly',
|
||||
}, {
|
||||
// Preview file request from core
|
||||
urlPattern: /^.*\/core\/preview\?fileId=.*/,
|
||||
handler: 'CacheFirst',
|
||||
|
||||
options: {
|
||||
cacheName: 'images',
|
||||
expiration: {
|
||||
maxAgeSeconds: 3600 * 24 * 7, // one week
|
||||
maxEntries: 20000, // 20k images
|
||||
},
|
||||
},
|
||||
...imageCacheOpts(7),
|
||||
}, {
|
||||
// Albums from Photos
|
||||
urlPattern: /^.*\/apps\/photos\/api\/v1\/preview\/.*/,
|
||||
...imageCacheOpts(7),
|
||||
}, {
|
||||
// Face previews from Memories
|
||||
urlPattern: /^.*\/apps\/memories\/api\/faces\/preview\/.*/,
|
||||
...imageCacheOpts(1),
|
||||
}, {
|
||||
// Match page requests
|
||||
urlPattern: /^.*\/.*$/,
|
||||
|
|
Loading…
Reference in New Issue