Add basic folder share stuff

old-stable24
Varun Patil 2022-10-28 17:25:39 -07:00
parent 2291bd4495
commit 87e96141c4
13 changed files with 170 additions and 65 deletions

View File

@ -24,6 +24,9 @@ return [
'defaults' => [ 'name' => '' ] 'defaults' => [ 'name' => '' ]
], ],
// Public pages
['name' => 'page#sharedFolder', 'url' => '/s/{token}', 'verb' => 'GET'],
// API // API
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'], ['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
['name' => 'api#dayPost', 'url' => '/api/days', 'verb' => 'POST'], ['name' => 'api#dayPost', 'url' => '/api/days', 'verb' => 'POST'],

View File

@ -43,6 +43,7 @@ use OCP\IDBConnection;
use OCP\IPreview; use OCP\IPreview;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\Share\IManager as IShareManager;
class ApiController extends Controller class ApiController extends Controller
{ {
@ -53,6 +54,7 @@ class ApiController extends Controller
private IAppManager $appManager; private IAppManager $appManager;
private TimelineQuery $timelineQuery; private TimelineQuery $timelineQuery;
private TimelineWrite $timelineWrite; private TimelineWrite $timelineWrite;
private IShareManager $shareManager;
private IPreview $preview; private IPreview $preview;
public function __construct( public function __construct(
@ -62,6 +64,7 @@ class ApiController extends Controller
IDBConnection $connection, IDBConnection $connection,
IRootFolder $rootFolder, IRootFolder $rootFolder,
IAppManager $appManager, IAppManager $appManager,
IShareManager $shareManager,
IPreview $preview IPreview $preview
) { ) {
parent::__construct(Application::APPNAME, $request); parent::__construct(Application::APPNAME, $request);
@ -71,6 +74,7 @@ class ApiController extends Controller
$this->connection = $connection; $this->connection = $connection;
$this->rootFolder = $rootFolder; $this->rootFolder = $rootFolder;
$this->appManager = $appManager; $this->appManager = $appManager;
$this->shareManager = $shareManager;
$this->previewManager = $preview; $this->previewManager = $preview;
$this->timelineQuery = new TimelineQuery($this->connection); $this->timelineQuery = new TimelineQuery($this->connection);
$this->timelineWrite = new TimelineWrite($connection, $preview); $this->timelineWrite = new TimelineWrite($connection, $preview);
@ -78,22 +82,28 @@ class ApiController extends Controller
/** /**
* @NoAdminRequired * @NoAdminRequired
*
* @PublicPage
*/ */
public function days(): JSONResponse public function days(): JSONResponse
{ {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (null === $user) { if (null === $user && !$this->getShareToken()) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
} }
$uid = $user->getUID(); $uid = $user ? $user->getUID() : '';
// Get the folder to show // Get the folder to show
$folder = $this->getRequestFolder(); $folder = null;
try {
$folder = $this->getRequestFolder();
} catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}
$recursive = null === $this->request->getParam('folder'); $recursive = null === $this->request->getParam('folder');
$archive = null !== $this->request->getParam('archive'); $archive = null !== $this->request->getParam('archive');
if (null === $folder) {
return new JSONResponse(['message' => 'Folder not found'], Http::STATUS_NOT_FOUND);
}
// Remove folder if album // Remove folder if album
// Permissions will be checked during the transform // Permissions will be checked during the transform
@ -127,6 +137,8 @@ class ApiController extends Controller
/** /**
* @NoAdminRequired * @NoAdminRequired
*
* @PublicPage
*/ */
public function dayPost(): JSONResponse public function dayPost(): JSONResponse
{ {
@ -140,14 +152,16 @@ class ApiController extends Controller
/** /**
* @NoAdminRequired * @NoAdminRequired
*
* @PublicPage
*/ */
public function day(string $id): JSONResponse public function day(string $id): JSONResponse
{ {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if (null === $user) { if (null === $user && !$this->getShareToken()) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
} }
$uid = $user->getUID(); $uid = $user ? $user->getUID() : '';
// Check for wildcard // Check for wildcard
$day_ids = []; $day_ids = [];
@ -664,6 +678,8 @@ class ApiController extends Controller
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
* @PublicPage
*
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function serviceWorker(): StreamResponse public function serviceWorker(): StreamResponse
@ -697,6 +713,11 @@ class ApiController extends Controller
$transforms[] = [$this->timelineQuery, 'transformExtraFields', $fields]; $transforms[] = [$this->timelineQuery, 'transformExtraFields', $fields];
} }
// Other transforms not allowed for public shares
if (null === $this->userSession->getUser()) {
return $transforms;
}
// Filter only favorites // Filter only favorites
if ($this->request->getParam('fav')) { if ($this->request->getParam('fav')) {
$transforms[] = [$this->timelineQuery, 'transformFavoriteFilter']; $transforms[] = [$this->timelineQuery, 'transformFavoriteFilter'];
@ -753,7 +774,9 @@ class ApiController extends Controller
*/ */
private function preloadDays(array &$days, &$folder, bool $recursive, bool $archive) private function preloadDays(array &$days, &$folder, bool $recursive, bool $archive)
{ {
$uid = $this->userSession->getUser()->getUID(); $user = $this->userSession->getUser();
$uid = $user ? $user->getUID() : '';
$transforms = $this->getTransformations(false); $transforms = $this->getTransformations(false);
$preloaded = 0; $preloaded = 0;
$preloadDayIds = []; $preloadDayIds = [];
@ -799,33 +822,49 @@ class ApiController extends Controller
/** Get the Folder object relevant to the request */ /** Get the Folder object relevant to the request */
private function getRequestFolder() private function getRequestFolder()
{ {
$uid = $this->userSession->getUser()->getUID(); $user = $this->userSession->getUser();
if (null === $user) {
// Public shares only
if ($token = $this->getShareToken()) {
$share = $this->shareManager->getShareByToken($token)->getNode(); // throws exception if not found
if (!$share instanceof Folder) {
throw new \Exception('Share not found or invalid');
}
try { return $share;
$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);
} }
if (!$folder instanceof Folder) {
throw new \Exception('Folder not found');
}
} catch (\Exception $e) {
return null; return null;
} }
$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);
}
if (!$folder instanceof Folder) {
throw new \Exception('Folder not found');
}
return $folder; return $folder;
} }
private function getShareToken()
{
return $this->request->getParam('folder_share');
}
/** /**
* Check if albums are enabled for this user. * Check if albums are enabled for this user.
*/ */

View File

@ -8,6 +8,7 @@ use OCA\Viewer\Event\LoadViewer;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState; use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
@ -103,6 +104,31 @@ class PageController extends Controller
return $response; return $response;
} }
/**
* @PublicPage
*
* @NoCSRFRequired
*/
public function sharedFolder(string $token)
{
// Scripts
Util::addScript($this->appName, 'memories-main');
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
$this->eventDispatcher->dispatchTyped(new LoadViewer());
// App version
$this->initialState->provideInitialState('version', $this->appManager->getAppInfo('memories')['version']);
$policy = new ContentSecurityPolicy();
$policy->addAllowedWorkerSrcDomain("'self'");
$policy->addAllowedScriptDomain("'self'");
$response = new PublicTemplateResponse($this->appName, 'main');
$response->setContentSecurityPolicy($policy);
return $response;
}
/** /**
* @NoAdminRequired * @NoAdminRequired
* *

View File

@ -102,7 +102,7 @@ trait TimelineQueryDays
// We don't actually use m.datetaken here, but postgres // We don't actually use m.datetaken here, but postgres
// needs that all fields in ORDER BY are also in SELECT // needs that all fields in ORDER BY are also in SELECT
// when using DISTINCT on selected fields // when using DISTINCT on selected fields
$query->select($fileid, 'f.etag', 'm.isvideo', 'vco.categoryid', 'm.datetaken', 'm.dayid', 'm.w', 'm.h') $query->select($fileid, 'f.etag', 'f.path', 'm.isvideo', 'vco.categoryid', 'm.datetaken', 'm.dayid', 'm.w', 'm.h')
->from('memories', 'm') ->from('memories', 'm')
; ;
$query = $this->joinFilecache($query, $folder, $recursive, $archive); $query = $this->joinFilecache($query, $folder, $recursive, $archive);
@ -129,7 +129,7 @@ trait TimelineQueryDays
$rows = $cursor->fetchAll(); $rows = $cursor->fetchAll();
$cursor->closeCursor(); $cursor->closeCursor();
return $this->processDay($rows); return $this->processDay($rows, $folder);
} }
/** /**
@ -150,10 +150,13 @@ trait TimelineQueryDays
/** /**
* Process the single day response. * Process the single day response.
* *
* @param array $day * @param array $day
* @param null|Folder $folder
*/ */
private function processDay(&$day) private function processDay(&$day, $folder)
{ {
$basePath = null !== $folder ? $folder->getInternalPath() : '#__#__#';
foreach ($day as &$row) { foreach ($day as &$row) {
// We don't need date taken (see query builder) // We don't need date taken (see query builder)
unset($row['datetaken']); unset($row['datetaken']);
@ -172,6 +175,14 @@ trait TimelineQueryDays
} }
unset($row['categoryid']); unset($row['categoryid']);
// Check if path exists and starts with basePath and remove
if (isset($row['path']) && !empty($row['path'])) {
if (0 === strpos($row['path'], $basePath)) {
$row['filename'] = substr($row['path'], \strlen($basePath));
}
unset($row['path']);
}
// All transform processing // All transform processing
$this->processFace($row); $this->processFace($row);
} }

View File

@ -532,6 +532,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
query.set("fields", "basename,mimetype"); query.set("fields", "basename,mimetype");
} }
// Favorites
if (this.$route.name === "folder-share") {
query.set("folder_share", this.$route.params.token);
}
// Create query string and append to URL // Create query string and append to URL
const queryStr = query.toString(); const queryStr = query.toString();
if (queryStr) { if (queryStr) {

View File

@ -23,7 +23,7 @@
class="fill-block" class="fill-block"
:class="{ error: info.flag & c.FLAG_LOAD_FAIL }" :class="{ error: info.flag & c.FLAG_LOAD_FAIL }"
:key="'fpreview-' + info.fileid" :key="'fpreview-' + info.fileid"
:src="getPreviewUrl(info.fileid, info.etag, true, 256)" :src="getPreviewUrl(info, true, 256)"
@error="info.flag |= c.FLAG_LOAD_FAIL" @error="info.flag |= c.FLAG_LOAD_FAIL"
/> />
</div> </div>

View File

@ -40,16 +40,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Emit, Mixins, Watch } from "vue-property-decorator"; import Check from "vue-material-design-icons/Check.vue";
import { IDay, IPhoto } from "../../types"; import Star from "vue-material-design-icons/Star.vue";
import Video from "vue-material-design-icons/Video.vue";
import { getPhotosPreviewUrl, getPreviewUrl } from "../../services/FileUtils"; import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator";
import errorsvg from "../../assets/error.svg"; import errorsvg from "../../assets/error.svg";
import GlobalMixin from "../../mixins/GlobalMixin"; import GlobalMixin from "../../mixins/GlobalMixin";
import { getPreviewUrl } from "../../services/FileUtils";
import Check from "vue-material-design-icons/Check.vue"; import { IDay, IPhoto } from "../../types";
import Video from "vue-material-design-icons/Video.vue";
import Star from "vue-material-design-icons/Star.vue";
@Component({ @Component({
components: { components: {
@ -122,9 +120,7 @@ export default class Photo extends Mixins(GlobalMixin) {
) - 1; ) - 1;
} }
const fun = return getPreviewUrl(this.data, false, size);
this.$route.name === "albums" ? getPhotosPreviewUrl : getPreviewUrl;
return fun(this.data.fileid, this.data.etag, false, size);
} }
/** Set src with overlay face rect */ /** Set src with overlay face rect */

View File

@ -24,7 +24,7 @@
class="fill-block" class="fill-block"
:class="{ error: info.flag & c.FLAG_LOAD_FAIL }" :class="{ error: info.flag & c.FLAG_LOAD_FAIL }"
:key="'fpreview-' + info.fileid" :key="'fpreview-' + info.fileid"
:src="getPreviewUrl(info.fileid, info.etag)" :src="getPreviewUrl(info)"
@error="info.flag |= c.FLAG_LOAD_FAIL" @error="info.flag |= c.FLAG_LOAD_FAIL"
/> />
</div> </div>
@ -80,7 +80,7 @@ export default class Tag extends Mixins(GlobalMixin) {
this.refreshPreviews(); this.refreshPreviews();
} }
getPreviewUrl(fileid: number, etag: string) { getPreviewUrl(photo: IPhoto) {
if (this.isFace) { if (this.isFace) {
return generateUrl( return generateUrl(
"/apps/memories/api/faces/preview/" + this.data.fileid "/apps/memories/api/faces/preview/" + this.data.fileid
@ -88,10 +88,10 @@ export default class Tag extends Mixins(GlobalMixin) {
} }
if (this.isAlbum) { if (this.isAlbum) {
return getPhotosPreviewUrl(fileid, etag, true, 256); return getPhotosPreviewUrl(photo, true, 256);
} }
return getPreviewUrl(fileid, etag, true, 256); return getPreviewUrl(photo, true, 256);
} }
get isFace() { get isFace() {

View File

@ -89,7 +89,7 @@ import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue";
import { NcButton, NcListItem, NcLoadingIcon } from "@nextcloud/vue"; import { NcButton, NcListItem, NcLoadingIcon } from "@nextcloud/vue";
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from "@nextcloud/router";
import { getPhotosPreviewUrl } from "../../services/FileUtils"; import { getPhotosPreviewUrl } from "../../services/FileUtils";
import { IAlbum } from "../../types"; import { IAlbum, IPhoto } from "../../types";
import axios from "@nextcloud/axios"; import axios from "@nextcloud/axios";
@Component({ @Component({
@ -103,7 +103,13 @@ import axios from "@nextcloud/axios";
}, },
filters: { filters: {
toCoverUrl(fileId: string) { toCoverUrl(fileId: string) {
return getPhotosPreviewUrl(Number(fileId), "unknown", true, 256); return getPhotosPreviewUrl(
{
fileid: Number(fileId),
} as IPhoto,
true,
256
);
}, },
}, },
}) })

View File

@ -157,12 +157,7 @@ export default class OnThisDay extends Mixins(GlobalMixin) {
// Get random photo // Get random photo
year.preview ||= utils.randomChoice(year.photos); year.preview ||= utils.randomChoice(year.photos);
year.url = getPreviewUrl( year.url = getPreviewUrl(year.preview, false, 512);
year.preview.fileid,
year.preview.etag,
false,
512
);
} }
await this.$nextTick(); await this.$nextTick();

View File

@ -135,5 +135,14 @@ export default new Router({
window.open(generateUrl("/apps/maps"), "_blank"); window.open(generateUrl("/apps/maps"), "_blank");
}, },
}, },
{
path: "/s/:token",
component: Timeline,
name: "folder-share",
props: (route) => ({
rootTitle: t("memories", "Shared Folder"),
}),
},
], ],
}); });

View File

@ -19,9 +19,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
import camelcase from "camelcase";
import { isNumber } from "./NumberUtils";
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from "@nextcloud/router";
import camelcase from "camelcase";
import { IExtendedPhoto, IFileInfo, IPhoto } from "../types";
import { isNumber } from "./NumberUtils";
/** /**
* Get an url encoded path * Get an url encoded path
@ -133,29 +134,41 @@ const genFileInfo = function (obj) {
return fileInfo; return fileInfo;
}; };
/** Get preview URL from Nextcloud core */ /** Get preview URL from photo object */
const getPreviewUrl = function ( const getPreviewUrl = function (
fileid: number, photo: IPhoto | IFileInfo,
etag: string,
square: boolean, square: boolean,
size: number size: number
): string { ) {
const a = square ? "0" : "1"; const a = square ? "0" : "1";
// Public preview
if (vuerouter.currentRoute.name === "folder-share") {
const token = vuerouter.currentRoute.params.token;
return generateUrl(
`/apps/files_sharing/publicpreview/${token}?file=${photo.filename}&fileId=${photo.fileid}&x=${size}&y=${size}&a=${a}`
);
}
// Albums from Photos
if (vuerouter.currentRoute.name === "albums") {
return getPhotosPreviewUrl(photo, square, size);
}
return generateUrl( return generateUrl(
`/core/preview?fileId=${fileid}&c=${etag}&x=${size}&y=${size}&forceIcon=0&a=${a}` `/core/preview?fileId=${photo.fileid}&c=${photo.etag}&x=${size}&y=${size}&forceIcon=0&a=${a}`
); );
}; };
/** Get the preview URL from the photos app */ /** Get the preview URL from the photos app */
const getPhotosPreviewUrl = function ( const getPhotosPreviewUrl = function (
fileid: number, photo: IPhoto | IFileInfo,
etag: string,
square: boolean, square: boolean,
size: number size: number
): string { ): string {
const a = square ? "0" : "1"; const a = square ? "0" : "1";
return generateUrl( return generateUrl(
`/apps/photos/api/v1/preview/${fileid}?c=${etag}&x=${size}&y=${size}&forceIcon=0&a=${a}` `/apps/photos/api/v1/preview/${photo.fileid}?c=${photo.etag}&x=${size}&y=${size}&forceIcon=0&a=${a}`
); );
}; };

View File

@ -37,6 +37,8 @@ export type IPhoto = {
fileid: number; fileid: number;
/** Etag from server */ /** Etag from server */
etag?: string; etag?: string;
/** Path to file */
filename?: string;
/** Bit flags */ /** Bit flags */
flag: number; flag: number;
/** DayID from server */ /** DayID from server */