fix some path encoding issues

AuthorKonata <konata@posteo.jp>
Date
Commita779ebe85f4b847ab287a3b7a99ac2f3b8d430ee
Parent6aad7e9
6 files changed, 46 insertions(+), 28 deletions(-)
▾Mserver/src/index.ts
@@ -17,6 +17,7 @@ import { stat } from "node:fs/promises";
import parseRange from "range-parser";
import { createReadStream } from "node:fs";
import { MediaFormat } from "music-server-shared/types";
import { decodePath } from "music-server-shared/utils";
import { stringifyMap } from "music-server-shared/mapconversion";
import { randomUUIDv7 } from "bun";
@@ -51,7 +52,7 @@ const downloadHandler: DownloadHandler = async ({ headers, params, set, request
set.status = "Not Found";
return "NOT_FOUND";
}
const filePath = path.join(musicRoot, decodeURI(params["*"]));
const filePath = path.join(musicRoot, decodePath(params["*"]));
const fileScan = await getPathInfo(filePath);
if (!fileScan || fileScan instanceof ServerError) {
set.status = fileScan?.status || "Internal Server Error";
@@ -109,7 +110,7 @@ const app = setup
.get(
"/transcode/*",
async ({ request, query, set, params }) => {
const filePath = path.join(musicRoot, decodeURI(params["*"]));
const filePath = path.join(musicRoot, decodePath(params["*"]));
const fileScan = await getPathInfo(filePath);
if (!fileScan || fileScan instanceof ServerError) {
set.status = fileScan?.status || "Internal Server Error";
@@ -155,7 +156,7 @@ const app = setup
.get(
"/list/*",
async ({ params, set, query }) => {
const dirPath = path.join(musicRoot, decodeURI(params["*"]));
const dirPath = path.join(musicRoot, decodePath(params["*"]));
const fileList = await listFiles(dirPath, query.recursive || false);
if (fileList instanceof ServerError) {
set.status = fileList.status;
@@ -169,7 +170,7 @@ const app = setup
)
.get("/cover/*", async ({ params, set }) => {
set.status = "Not Found";
const dirPath = path.join(musicRoot, decodeURI(params["*"]));
const dirPath = path.join(musicRoot, decodePath(params["*"]));
const fileScanResult = await findCover(dirPath);
if ("path" in fileScanResult) {
set.headers["Content-Type"] = fileScanResult.info.mimeType;
▾Ashared/utils.ts
@@ -0,0 +1,14 @@
export function decodePath(path: string) {
return path
.split("/")
.map((segment) => decodeURIComponent(segment))
.join("/");
}
export function encodePath(path: string) {
return path
.split("/")
.map((segment) => encodeURIComponent(segment))
.join("/");
}
▾Mwebclient/src/App.tsx
@@ -12,6 +12,7 @@ import { MediaFormat } from "music-server-shared/types";
import PlaylistManager from "./components/PlaylistManager";
import { BUILD_HASH } from "./build-info";
import { createStore } from "solid-js/store";
import { decodePath, encodePath } from "music-server-shared/utils";
//TODO: search
//TODO: better touch controls?
@@ -69,7 +70,7 @@ const App: Component = () => {
const parts = currentDir().split("/").filter(Boolean);
setBreadcrumbs(["root", ...parts]);
// Update URL fragment when directory changes
window.location.hash = encodeURI(currentDir());
window.location.hash = encodePath(currentDir());
});
createEffect(() => {
@@ -285,7 +286,7 @@ const App: Component = () => {
audioElement.addEventListener("volumechange", () => setPlayerState("volume", audioElement.volume));
window.addEventListener("hashchange", () => {
setCurrentDir(decodeURI(window.location.hash.slice(1)));
setCurrentDir(decodePath(window.location.hash.slice(1)));
});
document.addEventListener("keydown", (e) => {
▾Mwebclient/src/components/FileBrowser.tsx
@@ -177,7 +177,6 @@ export default function FileBrowser(props: FileBrowserProps) {
if (savedFileBrowserButtons !== null) setShowActionButtons(savedFileBrowserButtons === "true");
createEffect(() => localStorage.setItem(LocalStorageValues.fileBrowserButtons, showActionButtons().toString()));
filelistElement.addEventListener("scrollend", () => {
scrollPositions[scrollPositions.length - 1].pos = filelistElement.scrollTop;
});
@@ -185,7 +184,11 @@ export default function FileBrowser(props: FileBrowserProps) {
return (
<>
<dialog class="duplicate-warning-dialog" ref={dialogElement} onclick={(e) => e.target.nodeName === "DIALOG" && dialogElement.close()}>
<dialog
class="duplicate-warning-dialog"
ref={dialogElement}
onclick={(e) => e.target.nodeName === "DIALOG" && dialogElement.close()}
>
<div>
Some of the items you're trying to add already exists in your current playlist
<div class="buttons">
@@ -199,7 +202,7 @@ export default function FileBrowser(props: FileBrowserProps) {
<For each={props.breadcrumbs()}>
{(crumb, index) => (
<>
<span onClick={() => handleBreadcrumbClick(index())}>{decodeURIComponent(crumb)}</span>
<span onClick={() => handleBreadcrumbClick(index())}>{crumb}</span>
{index() < props.breadcrumbs().length - 1 && "/"}
</>
)}
▾Mwebclient/src/index.tsx
@@ -96,5 +96,6 @@ if (version && version !== currentVersion) {
})
.catch((e) => {
root.innerHTML = e.toString();
console.log(e)
});
}
▾Mwebclient/src/offline.ts
@@ -11,6 +11,7 @@ import { joinPath, toast } from "./utils";
import type { Accessor, ResourceActions } from "solid-js";
import { createStore } from "solid-js/store";
import { AsyncSemaphore } from "./semaphore";
import { encodePath } from "music-server-shared/utils";
interface Progress {
bytes: number;
@@ -131,7 +132,7 @@ export const clearDirectoryCache = (path: string) => {
};
async function listOnlineFiles(dir: string, recursive: boolean): Promise<FileListingWithStatus | undefined> {
const response = await fetch(`/list/${dir}?recursive=${recursive}`).catch(() => {});
const response = await fetch(`/list/${encodePath(dir)}?recursive=${recursive}`).catch(() => {});
if (response?.status === 404) {
toast(`Directory not found: ${dir}`, "error");
return;
@@ -223,7 +224,7 @@ export async function getCover(dir: string, offline: boolean) {
if (offline) return undefined;
const response = await fetch(`/cover/${dir}`, { method: "GET" }).catch(() => {});
const response = await fetch(`/cover/${encodePath(dir)}`, { method: "GET" }).catch(() => {});
if (!response || response.status === 404) {
db.put(CoverStore, { path: dir, data: null });
return undefined;
@@ -239,19 +240,22 @@ export const clearCoverCache = async (path: string) => {
};
//returns an object url to the blob if synced, otherwise the server url
export async function getSrc(path: string, options: AppOptions, offline: boolean) {
const db = await openDB();
const dbResult = (await db.get(FileStore, path)) as StoredFile;
if (dbResult) return URL.createObjectURL(dbResult.data);
export async function getSrc(path: string, options: AppOptions, offline: boolean, forceUrl?: true): Promise<string>;
export async function getSrc(path: string, options: AppOptions, offline: boolean, forceUrl?: boolean) {
if (!forceUrl) {
const db = await openDB();
const dbResult = (await db.get(FileStore, path)) as StoredFile;
if (dbResult) return URL.createObjectURL(dbResult.data);
if (offline) {
toast("This file is not cached, leave offline mode to play it", "error");
return undefined;
if (offline) {
toast("This file is not cached, leave offline mode to play it", "error");
return undefined;
}
}
if (options.enableTranscoding) {
return `${joinPath("/transcode", path)}?bitrate=${options.bitrate}&format=${options.format}&disableChunkedTranscoding=${options.disableChunkedTranscoding}`;
return `${joinPath("/transcode", encodePath(path))}?bitrate=${options.bitrate}&format=${options.format}&disableChunkedTranscoding=${options.disableChunkedTranscoding}`;
}
return joinPath("/download", path);
return joinPath("/download", encodePath(path));
}
export async function loadPlaylists() {
@@ -326,14 +330,8 @@ async function downloadFile(
try {
//make sure to only allow the set amount of parallel downloads
await downloadSemaphore.acquire();
let res: Response;
if (options.enableTranscoding) {
res = await fetch(
`${joinPath("/transcode", path)}?bitrate=${options.bitrate}&format=${options.format}&disableChunkedTranscoding=${options.disableChunkedTranscoding}`,
);
} else {
res = await fetch(joinPath("/download", path));
}
const res = await fetch(await getSrc(path, options, false, true));
//let the browser do the actual download
const blobPromise = res.clone().blob();
if (!res.ok || !res.body) throw res.statusText;