Merge 74099e06f7
into a882fb6c85
commit
941d285ec3
|
@ -2,14 +2,22 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
import { upload as postTus, useTus } from "./tus";
|
import { upload as postTus, useTus } from "./tus";
|
||||||
import { createURL, fetchURL, removePrefix } from "./utils";
|
import { createURL, fetchURL, removePrefix, StatusError } from "./utils";
|
||||||
|
|
||||||
export async function fetch(url: string) {
|
export async function fetch(url: string, signal?: AbortSignal) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
const res = await fetchURL(`/api/resources${url}`, { signal });
|
||||||
|
|
||||||
const res = await fetchURL(`/api/resources${url}`, {});
|
let data: Resource;
|
||||||
|
try {
|
||||||
const data = (await res.json()) as Resource;
|
data = (await res.json()) as Resource;
|
||||||
|
} catch (e) {
|
||||||
|
// Check if the error is an intentional cancellation
|
||||||
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
throw new StatusError("000 No connection", 0, true);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
data.url = `/files${url}`;
|
data.url = `/files${url}`;
|
||||||
|
|
||||||
if (data.isDir) {
|
if (data.isDir) {
|
||||||
|
@ -210,10 +218,18 @@ export function getSubtitlesURL(file: ResourceItem) {
|
||||||
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
|
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function usage(url: string) {
|
export async function usage(url: string, signal: AbortSignal) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(`/api/usage${url}`, {});
|
const res = await fetchURL(`/api/usage${url}`, { signal });
|
||||||
|
|
||||||
return await res.json();
|
try {
|
||||||
|
return await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
// Check if the error is an intentional cancellation
|
||||||
|
if (e instanceof Error && e.name == "AbortError") {
|
||||||
|
throw new StatusError("000 No connection", 0, true);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { fetchURL, removePrefix } from "./utils";
|
import { fetchURL, removePrefix, StatusError } from "./utils";
|
||||||
import url from "../utils/url";
|
import url from "../utils/url";
|
||||||
|
|
||||||
export default async function search(base: string, query: string) {
|
export default async function search(
|
||||||
|
base: string,
|
||||||
|
query: string,
|
||||||
|
signal: AbortSignal,
|
||||||
|
callback: (item: UploadItem) => void
|
||||||
|
) {
|
||||||
base = removePrefix(base);
|
base = removePrefix(base);
|
||||||
query = encodeURIComponent(query);
|
query = encodeURIComponent(query);
|
||||||
|
|
||||||
|
@ -9,19 +14,43 @@ export default async function search(base: string, query: string) {
|
||||||
base += "/";
|
base += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
const res = await fetchURL(`/api/search${base}?query=${query}`, { signal });
|
||||||
|
if (!res.body) {
|
||||||
|
throw new StatusError("000 No connection", 0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||||
|
let buffer = "";
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (value) {
|
||||||
|
buffer += value;
|
||||||
|
}
|
||||||
|
const lines = buffer.split(/\n/);
|
||||||
|
let lastLine = lines.pop();
|
||||||
|
// Save incomplete last line
|
||||||
|
if (!lastLine) {
|
||||||
|
lastLine = "";
|
||||||
|
}
|
||||||
|
buffer = lastLine;
|
||||||
|
|
||||||
let data = await res.json();
|
for (const line of lines) {
|
||||||
|
if (line) {
|
||||||
data = data.map((item: UploadItem) => {
|
const item = JSON.parse(line) as UploadItem;
|
||||||
item.url = `/files${base}` + url.encodePath(item.path);
|
item.url = `/files${base}` + url.encodePath(item.path);
|
||||||
|
if (item.dir) {
|
||||||
if (item.dir) {
|
item.url += "/";
|
||||||
item.url += "/";
|
}
|
||||||
|
callback(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (done) break;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
return item;
|
// Check if the error is an intentional cancellation
|
||||||
});
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
throw new StatusError("000 No connection", 0, true);
|
||||||
return data;
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import { encodePath } from "@/utils/url";
|
||||||
export class StatusError extends Error {
|
export class StatusError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: any,
|
message: any,
|
||||||
public status?: number
|
public status?: number,
|
||||||
|
public is_canceled?: boolean
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "StatusError";
|
this.name = "StatusError";
|
||||||
|
@ -33,7 +34,11 @@ export async function fetchURL(
|
||||||
},
|
},
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
// Check if the error is an intentional cancellation
|
||||||
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
throw new StatusError("000 No connection", 0, true);
|
||||||
|
}
|
||||||
throw new StatusError("000 No connection", 0);
|
throw new StatusError("000 No connection", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
v-if="active"
|
v-if="active"
|
||||||
class="action"
|
class="action"
|
||||||
@click="close"
|
@click="close"
|
||||||
:aria-label="$t('buttons.close')"
|
:aria-label="closeButtonTitle"
|
||||||
:title="$t('buttons.close')"
|
:title="closeButtonTitle"
|
||||||
>
|
>
|
||||||
<i class="material-icons">arrow_back</i>
|
<i v-if="ongoing" class="material-icons">stop_circle</i>
|
||||||
|
<i v-else class="material-icons">arrow_back</i>
|
||||||
</button>
|
</button>
|
||||||
<i v-else class="material-icons">search</i>
|
<i v-else class="material-icons">search</i>
|
||||||
<input
|
<input
|
||||||
|
@ -21,6 +22,15 @@
|
||||||
:aria-label="$t('search.search')"
|
:aria-label="$t('search.search')"
|
||||||
:placeholder="$t('search.search')"
|
:placeholder="$t('search.search')"
|
||||||
/>
|
/>
|
||||||
|
<i
|
||||||
|
v-show="ongoing"
|
||||||
|
class="material-icons spin"
|
||||||
|
style="display: inline-block"
|
||||||
|
>autorenew
|
||||||
|
</i>
|
||||||
|
<span style="margin-top: 5px" v-show="results.length > 0">
|
||||||
|
{{ results.length }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="result" ref="result">
|
<div id="result" ref="result">
|
||||||
|
@ -57,9 +67,6 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p id="renew">
|
|
||||||
<i class="material-icons spin">autorenew</i>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -70,10 +77,11 @@ import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { search } from "@/api";
|
import { search } from "@/api";
|
||||||
import { computed, inject, onMounted, ref, watch } from "vue";
|
import { computed, inject, onMounted, ref, watch, onUnmounted } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
import { StatusError } from "@/api/utils.ts";
|
||||||
|
|
||||||
const boxes = {
|
const boxes = {
|
||||||
image: { label: "images", icon: "insert_photo" },
|
image: { label: "images", icon: "insert_photo" },
|
||||||
|
@ -84,6 +92,7 @@ const boxes = {
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
|
let searchAbortController = new AbortController();
|
||||||
|
|
||||||
const { currentPromptName } = storeToRefs(layoutStore);
|
const { currentPromptName } = storeToRefs(layoutStore);
|
||||||
|
|
||||||
|
@ -124,9 +133,7 @@ watch(currentPromptName, (newVal, oldVal) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(prompt, () => {
|
watch(prompt, () => {
|
||||||
if (results.value.length) {
|
reset();
|
||||||
reset();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ...mapState(useFileStore, ["isListing"]),
|
// ...mapState(useFileStore, ["isListing"]),
|
||||||
|
@ -149,6 +156,10 @@ const filteredResults = computed(() => {
|
||||||
return results.value.slice(0, resultsCount.value);
|
return results.value.slice(0, resultsCount.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const closeButtonTitle = computed(() => {
|
||||||
|
return ongoing.value ? t("buttons.stopSearch") : t("buttons.close");
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (result.value === null) {
|
if (result.value === null) {
|
||||||
return;
|
return;
|
||||||
|
@ -164,14 +175,23 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
abortLastSearch();
|
||||||
|
});
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
!active.value && layoutStore.showHover("search");
|
!active.value && layoutStore.showHover("search");
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = (event: Event) => {
|
const close = (event: Event) => {
|
||||||
event.stopPropagation();
|
if (ongoing.value) {
|
||||||
event.preventDefault();
|
abortLastSearch();
|
||||||
layoutStore.closeHovers();
|
ongoing.value = false;
|
||||||
|
} else {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
layoutStore.closeHovers();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyup = (event: KeyboardEvent) => {
|
const keyup = (event: KeyboardEvent) => {
|
||||||
|
@ -188,11 +208,16 @@ const init = (string: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
abortLastSearch();
|
||||||
ongoing.value = false;
|
ongoing.value = false;
|
||||||
resultsCount.value = 50;
|
resultsCount.value = 50;
|
||||||
results.value = [];
|
results.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const abortLastSearch = () => {
|
||||||
|
searchAbortController.abort();
|
||||||
|
};
|
||||||
|
|
||||||
const submit = async (event: Event) => {
|
const submit = async (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
@ -208,8 +233,16 @@ const submit = async (event: Event) => {
|
||||||
ongoing.value = true;
|
ongoing.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
results.value = await search(path, prompt.value);
|
abortLastSearch();
|
||||||
|
searchAbortController = new AbortController();
|
||||||
|
results.value = [];
|
||||||
|
await search(path, prompt.value, searchAbortController.signal, (item) =>
|
||||||
|
results.value.push(item)
|
||||||
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (error instanceof StatusError && error.is_canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$showError(error);
|
$showError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,7 @@ import {
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import ProgressBar from "@/components/ProgressBar.vue";
|
import ProgressBar from "@/components/ProgressBar.vue";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import { StatusError } from "@/api/utils.js";
|
||||||
|
|
||||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||||
|
|
||||||
|
@ -136,7 +137,7 @@ export default {
|
||||||
name: "sidebar",
|
name: "sidebar",
|
||||||
setup() {
|
setup() {
|
||||||
const usage = reactive(USAGE_DEFAULT);
|
const usage = reactive(USAGE_DEFAULT);
|
||||||
return { usage };
|
return { usage, usageAbortController: new AbortController() };
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
|
@ -157,6 +158,9 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
||||||
|
abortOngoingFetchUsage() {
|
||||||
|
this.usageAbortController.abort();
|
||||||
|
},
|
||||||
async fetchUsage() {
|
async fetchUsage() {
|
||||||
const path = this.$route.path.endsWith("/")
|
const path = this.$route.path.endsWith("/")
|
||||||
? this.$route.path
|
? this.$route.path
|
||||||
|
@ -166,13 +170,18 @@ export default {
|
||||||
return Object.assign(this.usage, usageStats);
|
return Object.assign(this.usage, usageStats);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const usage = await api.usage(path);
|
this.abortOngoingFetchUsage();
|
||||||
|
this.usageAbortController = new AbortController();
|
||||||
|
const usage = await api.usage(path, this.usageAbortController.signal);
|
||||||
usageStats = {
|
usageStats = {
|
||||||
used: prettyBytes(usage.used, { binary: true }),
|
used: prettyBytes(usage.used, { binary: true }),
|
||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usage.total, { binary: true }),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof StatusError && error.is_canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$showError(error);
|
this.$showError(error);
|
||||||
}
|
}
|
||||||
return Object.assign(this.usage, usageStats);
|
return Object.assign(this.usage, usageStats);
|
||||||
|
@ -200,5 +209,8 @@ export default {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.abortOngoingFetchUsage();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { useFileStore } from "@/stores/file";
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files } from "@/api";
|
import { files } from "@/api";
|
||||||
|
import { StatusError } from "@/api/utils.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "file-list",
|
name: "file-list",
|
||||||
|
@ -43,6 +44,7 @@ export default {
|
||||||
},
|
},
|
||||||
selected: null,
|
selected: null,
|
||||||
current: window.location.pathname,
|
current: window.location.pathname,
|
||||||
|
nextAbortController: new AbortController(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ["$showError"],
|
inject: ["$showError"],
|
||||||
|
@ -56,7 +58,13 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fillOptions(this.req);
|
this.fillOptions(this.req);
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.abortOngoingNext();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
abortOngoingNext() {
|
||||||
|
this.nextAbortController.abort();
|
||||||
|
},
|
||||||
fillOptions(req) {
|
fillOptions(req) {
|
||||||
// Sets the current path and resets
|
// Sets the current path and resets
|
||||||
// the current items.
|
// the current items.
|
||||||
|
@ -94,8 +102,17 @@ export default {
|
||||||
// just clicked in and fill the options with its
|
// just clicked in and fill the options with its
|
||||||
// content.
|
// content.
|
||||||
const uri = event.currentTarget.dataset.url;
|
const uri = event.currentTarget.dataset.url;
|
||||||
|
this.abortOngoingNext();
|
||||||
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
|
this.nextAbortController = new AbortController();
|
||||||
|
files
|
||||||
|
.fetch(uri, this.nextAbortController.signal)
|
||||||
|
.then(this.fillOptions)
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof StatusError && e.is_canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$showError(e);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
touchstart(event) {
|
touchstart(event) {
|
||||||
const url = event.currentTarget.dataset.url;
|
const url = event.currentTarget.dataset.url;
|
||||||
|
|
|
@ -42,7 +42,8 @@
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"openFile": "Open file",
|
"openFile": "Open file",
|
||||||
"discardChanges": "Discard"
|
"discardChanges": "Discard",
|
||||||
|
"stopSearch": "Stop searching"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download File",
|
"downloadFile": "Download File",
|
||||||
|
|
|
@ -39,7 +39,8 @@
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"upload": "アップロード",
|
"upload": "アップロード",
|
||||||
"openFile": "ファイルを開く",
|
"openFile": "ファイルを開く",
|
||||||
"continue": "続行"
|
"continue": "続行",
|
||||||
|
"stopSearch": "検索を停止"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "ファイルのダウンロード",
|
"downloadFile": "ファイルのダウンロード",
|
||||||
|
|
|
@ -33,7 +33,8 @@
|
||||||
"switchView": "보기 전환",
|
"switchView": "보기 전환",
|
||||||
"toggleSidebar": "사이드바 전환",
|
"toggleSidebar": "사이드바 전환",
|
||||||
"update": "업데이트",
|
"update": "업데이트",
|
||||||
"upload": "업로드"
|
"upload": "업로드",
|
||||||
|
"stopSearch": "검색 중지"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "파일 다운로드",
|
"downloadFile": "파일 다운로드",
|
||||||
|
|
|
@ -37,7 +37,8 @@
|
||||||
"toggleSidebar": "Боковая панель",
|
"toggleSidebar": "Боковая панель",
|
||||||
"update": "Обновить",
|
"update": "Обновить",
|
||||||
"upload": "Загрузить",
|
"upload": "Загрузить",
|
||||||
"openFile": "Открыть файл"
|
"openFile": "Открыть файл",
|
||||||
|
"stopSearch": "Прекратить поиск"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Скачать файл",
|
"downloadFile": "Скачать файл",
|
||||||
|
|
|
@ -42,7 +42,8 @@
|
||||||
"openFile": "打开文件",
|
"openFile": "打开文件",
|
||||||
"continue": "继续",
|
"continue": "继续",
|
||||||
"fullScreen": "切换全屏",
|
"fullScreen": "切换全屏",
|
||||||
"discardChanges": "放弃更改"
|
"discardChanges": "放弃更改",
|
||||||
|
"stopSearch": "停止搜索"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "下载文件",
|
"downloadFile": "下载文件",
|
||||||
|
|
|
@ -41,7 +41,8 @@
|
||||||
"openFile": "開啟檔案",
|
"openFile": "開啟檔案",
|
||||||
"continue": "繼續",
|
"continue": "繼續",
|
||||||
"fullScreen": "切換全螢幕",
|
"fullScreen": "切換全螢幕",
|
||||||
"discardChanges": "放棄變更"
|
"discardChanges": "放棄變更",
|
||||||
|
"stopSearch": "停止搜尋"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "下載檔案",
|
"downloadFile": "下載檔案",
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface ApiOpts {
|
||||||
method?: ApiMethod;
|
method?: ApiMethod;
|
||||||
headers?: object;
|
headers?: object;
|
||||||
body?: any;
|
body?: any;
|
||||||
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TusSettings {
|
interface TusSettings {
|
||||||
|
|
|
@ -61,9 +61,7 @@ const route = useRoute();
|
||||||
|
|
||||||
const { t } = useI18n({});
|
const { t } = useI18n({});
|
||||||
|
|
||||||
const clean = (path: string) => {
|
let fetchDataController = new AbortController();
|
||||||
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = ref<StatusError | null>(null);
|
const error = ref<StatusError | null>(null);
|
||||||
|
|
||||||
|
@ -101,6 +99,7 @@ onUnmounted(() => {
|
||||||
layoutStore.toggleShell();
|
layoutStore.toggleShell();
|
||||||
}
|
}
|
||||||
fileStore.updateRequest(null);
|
fileStore.updateRequest(null);
|
||||||
|
fetchDataController.abort();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(route, (to, from) => {
|
watch(route, (to, from) => {
|
||||||
|
@ -142,20 +141,21 @@ const fetchData = async () => {
|
||||||
let url = route.path;
|
let url = route.path;
|
||||||
if (url === "") url = "/";
|
if (url === "") url = "/";
|
||||||
if (url[0] !== "/") url = "/" + url;
|
if (url[0] !== "/") url = "/" + url;
|
||||||
|
// Cancel the ongoing request
|
||||||
|
fetchDataController.abort();
|
||||||
|
fetchDataController = new AbortController();
|
||||||
try {
|
try {
|
||||||
const res = await api.fetch(url);
|
const res = await api.fetch(url, fetchDataController.signal);
|
||||||
|
|
||||||
if (clean(res.path) !== clean(`/${[...route.params.path].join("/")}`)) {
|
|
||||||
throw new Error("Data Mismatch!");
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStore.updateRequest(res);
|
fileStore.updateRequest(res);
|
||||||
document.title = `${res.name} - ${t("files.files")} - ${name}`;
|
document.title = `${res.name} - ${t("files.files")} - ${name}`;
|
||||||
|
layoutStore.loading = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof StatusError && err.is_canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
error.value = err;
|
error.value = err;
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
layoutStore.loading = false;
|
layoutStore.loading = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,82 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/search"
|
"github.com/filebrowser/filebrowser/v2/search"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const searchPingInterval = 5
|
||||||
|
|
||||||
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
response := []map[string]interface{}{}
|
response := make(chan map[string]interface{})
|
||||||
|
ctx, cancel := context.WithCancelCause(r.Context())
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// Avoid connection timeout
|
||||||
|
timeout := time.NewTimer(searchPingInterval * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
for {
|
||||||
|
var err error
|
||||||
|
var infoBytes []byte
|
||||||
|
select {
|
||||||
|
case info := <-response:
|
||||||
|
if info == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
infoBytes, err = json.Marshal(info)
|
||||||
|
case <-timeout.C:
|
||||||
|
// Send a heartbeat packet
|
||||||
|
infoBytes = nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
cancel(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = w.Write(infoBytes)
|
||||||
|
if err == nil {
|
||||||
|
_, err = w.Write([]byte("\n"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
cancel(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
query := r.URL.Query().Get("query")
|
query := r.URL.Query().Get("query")
|
||||||
|
|
||||||
err := search.Search(d.user.Fs, r.URL.Path, query, d, func(path string, f os.FileInfo) error {
|
err := search.Search(ctx, d.user.Fs, r.URL.Path, query, d, func(path string, f os.FileInfo) error {
|
||||||
response = append(response, map[string]interface{}{
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case response <- map[string]interface{}{
|
||||||
"dir": f.IsDir(),
|
"dir": f.IsDir(),
|
||||||
"path": path,
|
"path": path,
|
||||||
})
|
}:
|
||||||
|
}
|
||||||
return nil
|
return context.Cause(ctx)
|
||||||
})
|
})
|
||||||
|
close(response)
|
||||||
if err != nil {
|
wg.Wait()
|
||||||
|
if err == nil {
|
||||||
|
err = context.Cause(ctx)
|
||||||
|
}
|
||||||
|
// ignore cancellation errors from user aborts
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderJSON(w, r, response)
|
return 0, nil
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package search
|
package search
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -18,13 +19,17 @@ type searchOptions struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search searches for a query in a fs.
|
// Search searches for a query in a fs.
|
||||||
func Search(fs afero.Fs, scope, query string, checker rules.Checker, found func(path string, f os.FileInfo) error) error {
|
func Search(ctx context.Context,
|
||||||
|
fs afero.Fs, scope, query string, checker rules.Checker, found func(path string, f os.FileInfo) error) error {
|
||||||
search := parseSearch(query)
|
search := parseSearch(query)
|
||||||
|
|
||||||
scope = filepath.ToSlash(filepath.Clean(scope))
|
scope = filepath.ToSlash(filepath.Clean(scope))
|
||||||
scope = path.Join("/", scope)
|
scope = path.Join("/", scope)
|
||||||
|
|
||||||
return afero.Walk(fs, scope, func(fPath string, f os.FileInfo, _ error) error {
|
return afero.Walk(fs, scope, func(fPath string, f os.FileInfo, _ error) error {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return context.Cause(ctx)
|
||||||
|
}
|
||||||
fPath = filepath.ToSlash(filepath.Clean(fPath))
|
fPath = filepath.ToSlash(filepath.Clean(fPath))
|
||||||
fPath = path.Join("/", fPath)
|
fPath = path.Join("/", fPath)
|
||||||
relativePath := strings.TrimPrefix(fPath, scope)
|
relativePath := strings.TrimPrefix(fPath, scope)
|
||||||
|
|
Loading…
Reference in New Issue