big: add multipreview
parent
aecc528f38
commit
0f31f845fb
|
@ -56,6 +56,7 @@ return [
|
||||||
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
||||||
|
|
||||||
['name' => 'Image#preview', 'url' => '/api/image/preview/{id}', 'verb' => 'GET'],
|
['name' => 'Image#preview', 'url' => '/api/image/preview/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'Image#multipreview', 'url' => '/api/image/multipreview', 'verb' => 'POST'],
|
||||||
['name' => 'Image#info', 'url' => '/api/image/info/{id}', 'verb' => 'GET'],
|
['name' => 'Image#info', 'url' => '/api/image/info/{id}', 'verb' => 'GET'],
|
||||||
['name' => 'Image#setExif', 'url' => '/api/image/set-exif/{id}', 'verb' => 'PATCH'],
|
['name' => 'Image#setExif', 'url' => '/api/image/set-exif/{id}', 'verb' => 'PATCH'],
|
||||||
['name' => 'Image#jpeg', 'url' => '/api/image/jpeg/{id}', 'verb' => 'GET'],
|
['name' => 'Image#jpeg', 'url' => '/api/image/jpeg/{id}', 'verb' => 'GET'],
|
||||||
|
|
|
@ -29,6 +29,7 @@ use OCA\Memories\Exif;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
|
use OCP\Files\IRootFolder;
|
||||||
|
|
||||||
class ImageController extends ApiBase
|
class ImageController extends ApiBase
|
||||||
{
|
{
|
||||||
|
@ -76,6 +77,90 @@ class ImageController extends ApiBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* Get preview of many images
|
||||||
|
*/
|
||||||
|
public function multipreview() {
|
||||||
|
// read body to array
|
||||||
|
try {
|
||||||
|
$body = file_get_contents('php://input');
|
||||||
|
$files = json_decode($body, true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var \OCP\IPreview $previewManager */
|
||||||
|
$previewManager = \OC::$server->get(\OCP\IPreview::class);
|
||||||
|
|
||||||
|
// For checking max previews
|
||||||
|
$previewRoot = new \OC\Preview\Storage\Root(
|
||||||
|
\OC::$server->get(IRootFolder::class),
|
||||||
|
\OC::$server->getSystemConfig(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// stream the response
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Expires: ' . \gmdate('D, d M Y H:i:s \G\M\T', \time() + 7 * 3600 * 24));
|
||||||
|
header('Cache-Control: max-age=' . 7 * 3600 * 24 . ', private');
|
||||||
|
|
||||||
|
foreach ($files as $bodyFile) {
|
||||||
|
$reqid = $bodyFile['reqid'];
|
||||||
|
$fileid = (int) $bodyFile['fileid'];
|
||||||
|
$x = (int) $bodyFile['x'];
|
||||||
|
$y = (int) $bodyFile['y'];
|
||||||
|
$a = $bodyFile['a'] === '1';
|
||||||
|
|
||||||
|
$file = $this->getUserFile($fileid);
|
||||||
|
if (!$file) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure max preview exists
|
||||||
|
$fileId = (string) $file->getId();
|
||||||
|
$folder = $previewRoot->getFolder($fileId);
|
||||||
|
$hasMax = false;
|
||||||
|
foreach ($folder->getDirectoryListing() as $preview) {
|
||||||
|
$name = $preview->getName();
|
||||||
|
if (str_contains($name, '-max')) {
|
||||||
|
$hasMax = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$hasMax) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this preview to the response
|
||||||
|
try {
|
||||||
|
$preview = $previewManager->getPreview($file, $x, $y, !$a, 'fill');
|
||||||
|
$content = $preview->getContent();
|
||||||
|
if (empty($content)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'reqid' => $reqid,
|
||||||
|
'Content-Length' => \strlen($content),
|
||||||
|
'Content-Type' => $preview->getMimeType(),
|
||||||
|
]);
|
||||||
|
echo "\n";
|
||||||
|
echo $content;
|
||||||
|
flush();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { registerRoute } from "workbox-routing";
|
||||||
|
|
||||||
|
interface FetchPreviewObject {
|
||||||
|
url: URL;
|
||||||
|
fileid: number;
|
||||||
|
reqid: number;
|
||||||
|
callback: (blob: Response) => void;
|
||||||
|
}
|
||||||
|
let fetchPreviewQueue: FetchPreviewObject[] = [];
|
||||||
|
|
||||||
|
let imageCache: Cache;
|
||||||
|
(async () => {
|
||||||
|
imageCache = await caches.open("images");
|
||||||
|
})();
|
||||||
|
|
||||||
|
let fetchPreviewTimer: any;
|
||||||
|
async function flushPreviewQueue() {
|
||||||
|
if (fetchPreviewQueue.length === 0) return;
|
||||||
|
|
||||||
|
fetchPreviewTimer = 0;
|
||||||
|
const fetchPreviewQueueCopy = fetchPreviewQueue;
|
||||||
|
fetchPreviewQueue = [];
|
||||||
|
|
||||||
|
const files = fetchPreviewQueueCopy.map((p) => ({
|
||||||
|
fileid: p.fileid,
|
||||||
|
x: Number(p.url.searchParams.get("x")),
|
||||||
|
y: Number(p.url.searchParams.get("y")),
|
||||||
|
a: p.url.searchParams.get("a"),
|
||||||
|
reqid: p.reqid,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// infer the url from the first file
|
||||||
|
const firstUrl = fetchPreviewQueueCopy[0].url;
|
||||||
|
const url = new URL(firstUrl.toString());
|
||||||
|
const path = url.pathname.split("/");
|
||||||
|
const previewIndex = path.indexOf("preview");
|
||||||
|
url.pathname = path.slice(0, previewIndex).join("/") + "/multipreview";
|
||||||
|
url.searchParams.delete("x");
|
||||||
|
url.searchParams.delete("y");
|
||||||
|
url.searchParams.delete("a");
|
||||||
|
url.searchParams.delete("c");
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(files),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) throw new Error("Error fetching multi-preview");
|
||||||
|
const blob = await res.blob();
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
while (idx < blob.size) {
|
||||||
|
// Read a line of JSON from blob
|
||||||
|
const line = await blob.slice(idx, idx + 256).text();
|
||||||
|
const newlineIndex = line?.indexOf("\n");
|
||||||
|
const jsonParsed = JSON.parse(line?.slice(0, newlineIndex));
|
||||||
|
const imgLen = jsonParsed["Content-Length"];
|
||||||
|
const imgType = jsonParsed["Content-Type"];
|
||||||
|
const reqid = jsonParsed["reqid"];
|
||||||
|
idx += newlineIndex + 1;
|
||||||
|
|
||||||
|
console.debug("multi-preview", jsonParsed);
|
||||||
|
|
||||||
|
// Read the image data
|
||||||
|
const imgBlob = blob.slice(idx, idx + imgLen);
|
||||||
|
idx += imgLen;
|
||||||
|
|
||||||
|
// Initiate callbacks
|
||||||
|
fetchPreviewQueueCopy
|
||||||
|
.filter((p) => p.reqid === reqid)
|
||||||
|
.forEach((p) => {
|
||||||
|
p.callback(
|
||||||
|
new Response(imgBlob, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": imgType,
|
||||||
|
"Content-Length": imgLen,
|
||||||
|
Expires: res.headers.get("Expires"),
|
||||||
|
"Cache-Control": res.headers.get("Cache-Control"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
p.callback = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate callbacks for failed requests
|
||||||
|
fetchPreviewQueueCopy.forEach((fetchPreviewObject) => {
|
||||||
|
fetchPreviewObject.callback?.(
|
||||||
|
new Response("Image not found", {
|
||||||
|
status: 404,
|
||||||
|
statusText: "Image not found",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
/^.*\/apps\/memories\/api\/image\/preview\/.*/,
|
||||||
|
async ({ url }) => {
|
||||||
|
// Check if in cache
|
||||||
|
const cache = await imageCache?.match(url);
|
||||||
|
if (cache) return cache;
|
||||||
|
|
||||||
|
// Get file id from URL
|
||||||
|
const fileid = Number(url.pathname.split("/").pop());
|
||||||
|
|
||||||
|
// Aggregate requests
|
||||||
|
let res: Response = await new Promise((callback) => {
|
||||||
|
fetchPreviewQueue.push({
|
||||||
|
url,
|
||||||
|
fileid,
|
||||||
|
reqid: Math.random(),
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
if (!fetchPreviewTimer) {
|
||||||
|
fetchPreviewTimer = setTimeout(flushPreviewQueue, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
res = await fetch(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache response
|
||||||
|
if (res.status === 200) {
|
||||||
|
imageCache?.put(url, res.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
);
|
|
@ -5,6 +5,8 @@ import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
import './service-worker-custom';
|
||||||
|
|
||||||
registerRoute(/^.*\/apps\/memories\/api\/video\/transcode\/.*/, new NetworkOnly());
|
registerRoute(/^.*\/apps\/memories\/api\/video\/transcode\/.*/, new NetworkOnly());
|
||||||
registerRoute(/^.*\/apps\/memories\/api\/image\/jpeg\/.*/, new NetworkOnly());
|
registerRoute(/^.*\/apps\/memories\/api\/image\/jpeg\/.*/, new NetworkOnly());
|
||||||
registerRoute(/^.*\/remote.php\/.*/, new NetworkOnly());
|
registerRoute(/^.*\/remote.php\/.*/, new NetworkOnly());
|
||||||
|
|
Loading…
Reference in New Issue