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' => '' ]
],
// Public pages
['name' => 'page#sharedFolder', 'url' => '/s/{token}', 'verb' => 'GET'],
// API
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
['name' => 'api#dayPost', 'url' => '/api/days', 'verb' => 'POST'],

View File

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

View File

@ -102,7 +102,7 @@ trait TimelineQueryDays
// We don't actually use m.datetaken here, but postgres
// needs that all fields in ORDER BY are also in SELECT
// 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')
;
$query = $this->joinFilecache($query, $folder, $recursive, $archive);
@ -129,7 +129,7 @@ trait TimelineQueryDays
$rows = $cursor->fetchAll();
$cursor->closeCursor();
return $this->processDay($rows);
return $this->processDay($rows, $folder);
}
/**
@ -150,10 +150,13 @@ trait TimelineQueryDays
/**
* 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) {
// We don't need date taken (see query builder)
unset($row['datetaken']);
@ -172,6 +175,14 @@ trait TimelineQueryDays
}
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
$this->processFace($row);
}

View File

@ -532,6 +532,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
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
const queryStr = query.toString();
if (queryStr) {

View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@ import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue";
import { NcButton, NcListItem, NcLoadingIcon } from "@nextcloud/vue";
import { generateUrl } from "@nextcloud/router";
import { getPhotosPreviewUrl } from "../../services/FileUtils";
import { IAlbum } from "../../types";
import { IAlbum, IPhoto } from "../../types";
import axios from "@nextcloud/axios";
@Component({
@ -103,7 +103,13 @@ import axios from "@nextcloud/axios";
},
filters: {
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
year.preview ||= utils.randomChoice(year.photos);
year.url = getPreviewUrl(
year.preview.fileid,
year.preview.etag,
false,
512
);
year.url = getPreviewUrl(year.preview, false, 512);
}
await this.$nextTick();

View File

@ -135,5 +135,14 @@ export default new Router({
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/>.
*
*/
import camelcase from "camelcase";
import { isNumber } from "./NumberUtils";
import { generateUrl } from "@nextcloud/router";
import camelcase from "camelcase";
import { IExtendedPhoto, IFileInfo, IPhoto } from "../types";
import { isNumber } from "./NumberUtils";
/**
* Get an url encoded path
@ -133,29 +134,41 @@ const genFileInfo = function (obj) {
return fileInfo;
};
/** Get preview URL from Nextcloud core */
/** Get preview URL from photo object */
const getPreviewUrl = function (
fileid: number,
etag: string,
photo: IPhoto | IFileInfo,
square: boolean,
size: number
): string {
) {
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(
`/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 */
const getPhotosPreviewUrl = function (
fileid: number,
etag: string,
photo: IPhoto | IFileInfo,
square: boolean,
size: number
): string {
const a = square ? "0" : "1";
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;
/** Etag from server */
etag?: string;
/** Path to file */
filename?: string;
/** Bit flags */
flag: number;
/** DayID from server */