+ Subdirectory support

This commit is contained in:
Pogodaanton 2020-07-11 13:42:10 +02:00
parent bd604035a5
commit 4df949b770
23 changed files with 144 additions and 75 deletions

View File

@ -63,6 +63,7 @@
"build": "react-app-rewired build",
"test": "react-app-rewired test"
},
"homepage": "./",
"config-overrides-path": "scripts/config-overrides",
"browserslist": {
"production": [

View File

@ -4,17 +4,22 @@ IndexIgnore *
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# RewriteBase /
# Force trailing slash
RewriteCond %{REQUEST_URI} /+[^\.]+$
RewriteRule ^(.+[^/])$ %{REQUEST_URI}/ [R=301,L]
# Rewrite jpg/png/gif/webp file access to uploads folder
# Only rewrite in base directory
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}/uploads/$1.$2 -f
RewriteRule ^(.+)\.(jpg|png|gif|webp|mp4)$ uploads/$1.$2 [L]
RewriteRule ^([^\/]+)\.(jpg|png|gif|webp|mp4)$ uploads/$1.$2 [L]
# Rewrite requests going into an unknown /static/ folder
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)\/(static)\/(.+) static/$3 [L]
# Don't rewrite files or directories
RewriteRule ^index.php$ - [L]
@ -22,7 +27,7 @@ IndexIgnore *
RewriteCond %{REQUEST_FILENAME} !-d
# Rewrite everything else to index.php to allow html5 state links
RewriteRule . /index.php [L]
RewriteRule . index.php [L]
</IfModule>

View File

@ -1,6 +1,13 @@
<?php
require_once "./protected/output.inc.php";
session_start();
/**
* Determines in which directory Shadis is located in
*/
$base_directory = dirname($_SERVER['SCRIPT_NAME']);
$homepage = url_origin($_SERVER) . $base_directory;
$uri_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($uri_path, '/'));
$file_data = null;
@ -9,7 +16,6 @@ $title = "Shadis";
if (!empty($segments[0])) {
if (strlen($segments[0]) === 8) {
require_once "./protected/db.inc.php";
require_once "./protected/output.inc.php";
$file_data = $db->request_file($segments[0]);
$title = ($file_data["title"] !== "" ? ($file_data["title"] . " - ") : "") . $file_data["id"] . " - Shadis";
}
@ -20,20 +26,23 @@ if (!empty($segments[0])) {
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/static/media/favicon.ico" />
<link rel="icon" href="<?php echo $homepage . "/static/media/favicon.ico"; ?>" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta content="noindex" name="robots">
<link rel="apple-touch-icon" href="%PUBLIC_URL%/static/media/logo192.png" />
<link rel="apple-touch-icon" href="<?php echo $homepage . "/static/media/logo192.png"; ?>" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Shadis</title>
<link rel="manifest" href="<?php echo $homepage . "/manifest.json"; ?>" />
<title><?php echo $title; ?></title>
<meta name="title" content="<?php echo $title; ?>">
<meta name="description" content="Share and host your favourite screenshots and screencaptures on your own server!">
<?php
echo '<script>var baseDirectory = ';
echo json_encode($base_directory);
echo '</script>';
if (isset($_SESSION["u_id"])) {
$user_data = array("username" => $_SESSION["u_name"]);
echo '<script>var userData = ';
@ -42,8 +51,7 @@ if (!empty($segments[0])) {
}
if (!is_null($file_data)) :
$file_data["fromServer"] = true;
$origin_url = url_origin($_SERVER);
$file_url = $origin_url . "/" . $file_data["id"] . "." . $file_data["extension"];
$file_url = $homepage . "/" . $file_data["id"] . "." . $file_data["extension"];
echo '<script>var fileData = ';
echo json_encode($file_data);
@ -77,7 +85,7 @@ if (!empty($segments[0])) {
<meta property="og:image" content="<?php echo $file_url; ?>">
<meta property="og:image:width" content="<?php echo $file_data["width"]; ?>">
<meta property="og:image:height" content="<?php echo $file_data["height"]; ?>">
<meta property="og:url" content="<?php echo $origin_url . "/" . $file_data["id"] . "/"; ?>">
<meta property="og:url" content="<?php echo $homepage . "/" . $file_data["id"] . "/"; ?>">
<!-- Twitter Metadata -->
<meta name="twitter:card" content="summary_large_image">

View File

@ -37,14 +37,6 @@ define("UPLOAD_TOKEN", "your_secret_upload_token_here");
*/
$table_prefix = "shadis_";
/**
* Base directory of Shadis
*
* If you put Shadis in a location other than root,
* modify this variable to the specific subdirectory.
*/
$base_directory = "/";
/**
* Path to the upload directory of Shadis
*

View File

@ -15,6 +15,32 @@ process.on("unhandledRejection", err => {
throw err;
});
/**
* Rewire publicPath generator
*
* .htaccess reroutes requests of any undefined `static` directory to the `static`
* folder found in project root. Thus, we can make `publicPath` a relative value.
*/
require("react-dev-utils/getPublicUrlOrPath");
require.cache[require.resolve("react-dev-utils/getPublicUrlOrPath")].exports = (
isEnvDevelopment,
homepage
) => {
const { URL } = require("url");
const stubDomain = "https://create-react-app.dev";
if (homepage) {
// strip last slash if exists
homepage = homepage.endsWith("/") ? homepage : homepage + "/";
// validate if `homepage` is a URL or path like and use just pathname
const validHomepagePathname = new URL(homepage, stubDomain).pathname;
return homepage.startsWith(".") ? homepage : validHomepagePathname;
}
return "/";
};
const fs = require("fs-extra");
const paths = require("react-scripts/config/paths");
const path = require("path");
@ -27,8 +53,8 @@ const chalk = require("chalk");
/**
* Rewire compilation success screen
*/
const reactDevUtils = rewire("react-dev-utils/WebpackDevServerUtils");
reactDevUtils.__set__("printInstructions", (appName, urls, useYarn) => {
const webpackDevUtils = rewire("react-dev-utils/WebpackDevServerUtils");
webpackDevUtils.__set__("printInstructions", (appName, urls, useYarn) => {
console.log();
console.log(
chalk.cyan(" The development bundle was output to " + chalk.bold("dist"))
@ -133,7 +159,7 @@ const urls = {
};
// Create a webpack compiler that is configured with custom messages.
const compiler = reactDevUtils.createCompiler({
const compiler = webpackDevUtils.createCompiler({
appName,
config,
devSocket,

View File

@ -11,6 +11,7 @@ interface Window {
* @memberof Window
*/
userData?: { username: string };
/**
* THe backend sets a global "fileData" object
* which provides everything necessary
@ -33,6 +34,11 @@ interface Window {
fromServer?: boolean;
has_gif?: boolean;
};
/**
* The project's root path, defined by the server
*/
baseDirectory?: string;
}
declare module "react-resize-aware";

View File

@ -3,7 +3,7 @@ import { FileViewProps, FileData } from "../FileView/FileView.props";
import { FullscreenLoader } from "../Loader";
import { RouteChildrenProps } from "react-router-dom";
import React, { useRef, useState, useEffect } from "react";
import axios from "../_interceptedAxios";
import axios, { getApiPath } from "../_interceptedAxios";
import { AnimatePresence, AnimateSharedLayout } from "framer-motion";
import { isLoggedIn, toast, DesignToolkitProvider } from "../_DesignSystem";
import { useTranslation } from "react-i18next";
@ -58,7 +58,7 @@ const useFilePrefetcher = (id: string, history: History<{}>) => {
useEffect(() => {
const fetchFileData = async (id: string) => {
try {
const res = await axios.get(window.location.origin + "/api/get.php", {
const res = await axios.get(getApiPath("get"), {
params: { id },
});

View File

@ -33,7 +33,7 @@ const styles: ComponentStyles<AppContainerClassNameContract, DesignSystem> = {
const AppContainer: React.ComponentType<AppContainerProps> = ({ managedClasses }) => (
<div className={managedClasses.container}>
<Router>
<Router basename={window.baseDirectory}>
<Suspense fallback={null}>
<Route path={["/:id", "/"]} component={AnimatedRoutes} />
</Suspense>

View File

@ -4,7 +4,7 @@ import manageJss, { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import { DashboardClassNameContract, DashboardProps } from "./Dashboard.props";
import { Header, toast } from "../_DesignSystem";
import { withDropzone } from "../FullscreenDropzone/FullscreenDropzone";
import axios from "../_interceptedAxios";
import axios, { getApiPath } from "../_interceptedAxios";
import { useTranslation } from "react-i18next";
import { ListDataItem, DashboardListProps } from "../DashboardList/DashboardList.props";
import { FullscreenLoader } from "../Loader";
@ -43,7 +43,7 @@ const Dashboard: React.FC<DashboardProps> = props => {
useEffect(() => {
const updateFileList = async () => {
try {
const res = await axios.get(window.location.origin + "/api/getAll.php");
const res = await axios.get(getApiPath("getAll"));
setListData(res.data);
} catch (err) {
toast.error(t("error.listGeneric") + ":", t(err.i18n, err.message));
@ -56,7 +56,7 @@ const Dashboard: React.FC<DashboardProps> = props => {
const onDeleteSelected = async (selection: string[]) => {
try {
await axios.post(window.location.origin + "/api/edit.php", {
await axios.post(getApiPath("edit"), {
selection,
action: "delete",
});

View File

@ -27,6 +27,7 @@ import { classNames } from "@microsoft/fast-web-utilities";
import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import { FaPlayCircle } from "react-icons/fa";
import { basePath } from "../../_interceptedAxios";
const styles: ComponentStyles<DashboardListCellClassNameContract, DesignSystem> = {
dashboardListCell: {
@ -230,7 +231,7 @@ const CellRenderer: React.FC<DashboardListCellProps> = props => {
<Link to={`/${id}/`} onClick={shouldExecuteclick}>
<img
className={props.managedClasses.dashboardListCell_image}
src={`${window.location.origin}/${id}.thumb.jpg`}
src={`${basePath}/${id}.thumb.jpg`}
alt={!title || title === "untitled" ? t("untitled") : title}
onError={onImageError}
onLoad={onImageLoaded}

View File

@ -2,7 +2,7 @@ import React from "react";
import { FVSidebarFooterProps } from "./FVSidebarFooter.props";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import axios from "../../../_interceptedAxios";
import axios, { getApiPath } from "../../../_interceptedAxios";
import { Button, ButtonAppearance, toast } from "../../../_DesignSystem";
import { FaTrash } from "react-icons/fa";
@ -17,7 +17,7 @@ const FVSidebarDeleteButton: React.ComponentType<FVSidebarFooterProps> = ({
const onDelete = async () => {
try {
await axios.post(window.location.origin + "/api/edit.php", {
await axios.post(getApiPath("edit"), {
selection: fileData.id,
action: "delete",
});

View File

@ -11,7 +11,7 @@ import {
ProgressClassNameContract,
} from "@microsoft/fast-components-react-msft";
import { FaCheck, FaExclamationTriangle } from "react-icons/fa";
import axios from "../../../_interceptedAxios";
import axios, { getApiPath } from "../../../_interceptedAxios";
import { toast } from "../../../_DesignSystem";
import { useTranslation } from "react-i18next";
import { SidebarData } from "./FVSidebarContext";
@ -68,7 +68,7 @@ const FVSidebarDescEditor: React.ComponentType<FVSidebarDescEditorProps> = ({
const deferredLoading = setTimeout(() => setLoadingState("loading"), 500);
try {
await axios.post(window.location.origin + "/api/edit.php", {
await axios.post(getApiPath("edit"), {
selection: fileData.id,
value,
action: "editTitle",

View File

@ -19,6 +19,7 @@ import { designSystemContext } from "@microsoft/fast-jss-manager-react/dist/cont
import loadable from "@loadable/component";
import { IconType } from "react-icons/lib";
import FVSidebarConvertButton from "./FVSidebarConvertButton";
import { basePath } from "../../../_interceptedAxios";
const FVSidebarDeleteButton = loadable(() => import("./FVSidebarDeleteButton"));
@ -143,7 +144,7 @@ const FVSidebarFooter: React.ComponentType<FVSidebarFooterProps> = ({
<Button
appearance={ButtonAppearance.stealth}
icon={FaDownload}
href={`${window.location.origin}/${fileData.id}.${fileData.extension}`}
href={`${basePath}/${fileData.id}.${fileData.extension}`}
target="_blank"
download
>
@ -153,7 +154,7 @@ const FVSidebarFooter: React.ComponentType<FVSidebarFooterProps> = ({
jssStyleSheet={fileData.has_gif ? overlapIconButtonStyles : null}
appearance={ButtonAppearance.stealth}
icon={fileData.has_gif ? OverlapIcon(FaVideo) : FaLink}
href={`${window.location.origin}/${fileData.id}.${fileData.extension}`}
href={`${basePath}/${fileData.id}.${fileData.extension}`}
target="_blank"
>
{fileData.has_gif ? t("sourceVideo") : t("source")}
@ -163,7 +164,7 @@ const FVSidebarFooter: React.ComponentType<FVSidebarFooterProps> = ({
jssStyleSheet={overlapIconButtonStyles}
appearance={ButtonAppearance.stealth}
icon={OverlapIcon(FaImage)}
href={`${window.location.origin}/${fileData.id}.gif`}
href={`${basePath}/${fileData.id}.gif`}
target="_blank"
>
{t("sourceGif")}

View File

@ -23,6 +23,7 @@ import ImageViewerSlider from "./ImageViewerSlider";
import { TweenProps, spring } from "popmotion";
import { SidebarData, SidebarEventEmitter } from "../FVSidebar/FVSidebarContext";
import { ThumbnailContext, ssrContainer } from "../ThumbnailViewer/ThumbnailViewer";
import { basePath } from "../../../_interceptedAxios";
const applyCenteredAbsolute: CSSRules<DesignSystem> = {
position: "absolute",
@ -123,7 +124,7 @@ const ImageViewer: React.ComponentType<ImageViewerProps> = ({
addEntryFinishListener(() => {
image.addEventListener("load", listener);
image.src = `${window.location.origin}/${id}.${extension}`;
image.src = `${basePath}/${id}.${extension}`;
if (fromServer) {
if (image.complete) listener();

View File

@ -9,6 +9,7 @@ import manageJss, { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import { isLoggedIn, headerHeight, useScaleFactor } from "../../../_DesignSystem";
import { motion } from "framer-motion";
import { useViewportDimensions } from "../ImageViewer/useViewportDimensions";
import { basePath } from "../../../_interceptedAxios";
const styles: ComponentStyles<ThumbnailViewerClassNameContract, DesignSystem> = {
viewer: {
@ -172,7 +173,7 @@ const ThumbnailViewer: React.ComponentType<ThumbnailViewerProps> = ({
onAnimationStart={onMagicAnimStart}
onAnimationComplete={onMagicAnimEnd}
alt={title}
src={`${window.location.origin}/${id}.thumb.jpg`}
src={`${basePath}/${id}.thumb.jpg`}
style={{
width: defaultWidth,
height: defaultHeight,

View File

@ -8,6 +8,7 @@ import { SidebarData } from "../FVSidebar/FVSidebarContext";
import { useTranslation } from "react-i18next";
import { TabPanel } from "../../../_DesignSystem/Tabs/TabViewer/TabViewer.props";
import tabEventEmitter from "../../../_DesignSystem/Tabs/TabEvents";
import { basePath } from "../../../_interceptedAxios";
const styles: ComponentStyles<VideoViewerClassNameContract, DesignSystem> = {
videoViewer: {
@ -80,7 +81,7 @@ const VideoViewer: React.ComponentType<VideoViewerProps> = ({
addEntryFinishListener(() => {
videoEl.addEventListener("canplaythrough", listener);
videoEl.src = `${window.location.origin}/${id}.${extension}`;
videoEl.src = `${basePath}/${id}.${extension}`;
});
return () => {
@ -130,7 +131,7 @@ const VideoViewer: React.ComponentType<VideoViewerProps> = ({
toastId = toast.info(t("gif.load"), "", { progress: 0 });
}, 500) as any;
imgEl.src = `${window.location.origin}/${id}.gif`;
imgEl.src = `${basePath}/${id}.gif`;
}
}, [id, t]);

View File

@ -23,7 +23,7 @@ import {
} from "@microsoft/fast-components-styles-msft";
import { parseColorHexRGBA } from "@microsoft/fast-colors";
import { ProgressIcon, getScaleFactorByConstraints } from "../../_DesignSystem";
import axios from "../../_interceptedAxios";
import axios, { getApiPath } from "../../_interceptedAxios";
const DropzoneUploadStyles: ComponentStyles<
DropzoneUploadClassNameContract,
@ -194,11 +194,9 @@ const DropzoneUpload: React.ComponentType<DropzoneUploadProps> = React.memo(
setUploadState(true);
try {
const res = await axios.post(
window.location.origin + "/api/upload.php",
formData,
{ onUploadProgress }
);
const res = await axios.post(getApiPath("upload"), formData, {
onUploadProgress,
});
setResData(res.data);
} catch (err) {
console.log("An error happened!\n", err);

View File

@ -1,7 +1,7 @@
import React, { useEffect, useCallback, useRef, useState } from "react";
import { toast } from "../../_DesignSystem";
import gifJS from "gif.js";
import axios from "../../_interceptedAxios";
import axios, { getApiPath, basePath } from "../../_interceptedAxios";
import gifGenEventEmitter from "./VideoGifGeneratorEvents";
/**
@ -25,9 +25,7 @@ const desiredFramerate = 8;
*/
const gif = new gifJS({
workerScript:
window.location.origin +
"/static/js/" +
(canWasm ? "gif.worker-wasm.js" : "gif.worker.js"),
basePath + "/static/js/" + (canWasm ? "gif.worker-wasm.js" : "gif.worker.js"),
workers: 4,
quality: 8,
// globalPalette: true,
@ -118,7 +116,7 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
});
// NOTE: MP4 is hardcoded!
videoEl.src = `${window.location.origin}/${fileID}.mp4`;
videoEl.src = `${basePath}/${fileID}.mp4`;
document.body.appendChild(videoEl);
}, []);
@ -172,21 +170,17 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
postData.append("data", !isStitchRequest ? blobChunk : null);
try {
await axios.post(
window.location.origin + "/api/finishAdminTask.php",
postData,
{
onUploadProgress: p => {
const uploadPercent = (p.loaded * 100) / p.total;
const chunkCompletePercent = 34 / maxChunkAmount;
const progress =
(uploadPercent * chunkCompletePercent) / 100 +
66 +
chunkCompletePercent * chunkNum;
setProgress(progress);
},
}
);
await axios.post(getApiPath("finishAdminTask"), postData, {
onUploadProgress: p => {
const uploadPercent = (p.loaded * 100) / p.total;
const chunkCompletePercent = 34 / maxChunkAmount;
const progress =
(uploadPercent * chunkCompletePercent) / 100 +
66 +
chunkCompletePercent * chunkNum;
setProgress(progress);
},
});
if (!isStitchRequest) uploadGifChunk(chunkNum + 1);
else shiftToNextGif();

View File

@ -2,8 +2,10 @@ import React, { useEffect } from "react";
// eslint-disable-next-line import/no-webpack-loader-syntax
import VideoWorker from "worker-loader!./video.worker.ts";
import { toast } from "../../_DesignSystem";
import { basePath } from "../../_interceptedAxios";
let worker: VideoWorker = null;
const { baseDirectory } = window;
/**
* - Retrieve a list of missing thumbnails
@ -72,7 +74,7 @@ const VideoThumbnailGenerator: React.ComponentType<{}> = props => {
});
// NOTE: MP4 is hardcoded, keep that in mind
videoEl.src = `${window.location.origin}/${id}.mp4#t=0.1`;
videoEl.src = `${basePath}/${id}.mp4#t=0.1`;
document.body.appendChild(canvasEl);
document.body.appendChild(videoEl);
});
@ -87,7 +89,7 @@ const VideoThumbnailGenerator: React.ComponentType<{}> = props => {
case "getFirstFrame":
if (typeof data.arguments === "string") {
getFirstFrame(data.arguments).then(obj => {
worker.postMessage({ task: "setFrame", arguments: obj }, [
worker.postMessage({ task: "setFrame", arguments: obj, baseDirectory }, [
obj.arrayBuffer,
]);
});
@ -109,7 +111,7 @@ const VideoThumbnailGenerator: React.ComponentType<{}> = props => {
}, []);
useEffect(() => {
worker.postMessage("fetchList");
worker.postMessage({ task: "fetchList", baseDirectory });
}, []);
return null;

View File

@ -33,9 +33,12 @@ ctx.addEventListener("message", ({ data }) => {
case "fetchList":
(async () => {
try {
const res = await axios.get(self.location.origin + "/api/getAdminTasks.php", {
params: { type: "video-thumbnail" },
});
const res = await axios.get(
self.location.origin + data.baseDirectory + "/api/getAdminTasks.php",
{
params: { type: "video-thumbnail" },
}
);
if (res.data) {
IDList = res.data;
@ -64,7 +67,10 @@ ctx.addEventListener("message", ({ data }) => {
postData.append("id", id);
postData.append("data", imageBlob);
await axios.post(self.location.origin + "/api/finishAdminTask.php", postData);
await axios.post(
self.location.origin + data.baseDirectory + "/api/finishAdminTask.php",
postData
);
IDList.shift();
generateNextThumbnail();
} catch (err) {

View File

@ -1,4 +1,5 @@
import axiosInstance, { AxiosInstance, AxiosError } from "axios";
import { APIMethod } from "./index.types";
export interface CustomError {
code: number;
@ -6,6 +7,10 @@ export interface CustomError {
i18n: string;
}
/**
* A special instance of Axios which has a built-in
* error handler specifically made for API calls to shadis' backend server.
*/
const axios: AxiosInstance = axiosInstance.create();
axios.interceptors.response.use(
res => res,
@ -22,4 +27,16 @@ axios.interceptors.response.use(
}
);
/**
* Retrieves the root path of the client
*/
export const basePath: URL["href"] = window.location.origin + window.baseDirectory;
/**
* Generates a URL that can be used to request data from the backend API
*/
export const getApiPath = (path: APIMethod): URL["href"] => {
return basePath + "/api/" + path + ".php";
};
export default axios;

View File

@ -0,0 +1,9 @@
export type APIMethod =
| "get"
| "getAll"
| "edit"
| "getAdminTasks"
| "finishAdminTask"
| "login"
| "logout"
| "upload";

View File

@ -23,7 +23,7 @@ i18n
ns: [...defaultNs, ...(isLoggedIn ? ["dashboard"] : [])],
defaultNS: "common",
backend: {
loadPath: "/static/locales/{{lng}}/{{ns}}.json",
loadPath: "./static/locales/{{lng}}/{{ns}}.json",
},
detection: {
lookupQuerystring: "lang",