Files
Oxicloud/static/js/app/filesView.js
T

274 lines
9.1 KiB
JavaScript
Raw Normal View History

// @ts-check
import { i18n } from '../core/i18n.js';
import { inlineViewer } from '../features/files/inlineViewer.js';
import { multiSelect } from '../features/files/multiSelect.js';
import { resolveHomeFolder } from './authSession.js';
import { updateHistory } from './main.js';
import { app } from './state.js';
import { ui } from './ui.js';
import { uiNotifications } from './uiNotifications.js';
2026-05-07 23:40:02 +02:00
/** @import {FileItem, FolderItem} from '../core/types.js' */
2026-04-30 02:02:27 +02:00
let isLoadingFiles = false;
/**
* getFolder information
* @param {string} id the id of the folder
2026-05-07 23:40:02 +02:00
* @returns {Promise<FolderItem>}
*/
2026-04-07 22:48:59 +02:00
async function getFolder(id) {
/** @type {HeadersInit} */
const headers = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
2026-04-07 22:48:59 +02:00
Pragma: 'no-cache'
};
/** @type {RequestInit} */
const requestOptions = {
headers,
credentials: 'same-origin',
cache: 'no-store'
};
2026-04-07 22:48:59 +02:00
2026-04-07 22:50:42 +02:00
const folderInformations = await fetch(`/api/folders/${id}`, requestOptions);
if (folderInformations.ok) {
2026-04-07 22:48:59 +02:00
return folderInformations.json();
} else {
console.warn(`Error fetching folder ${id}`);
return Promise.reject(null);
}
}
/**
2026-05-22 10:15:25 +02:00
* Rebuild breadcrumb from selected folder (iterate up to root).
*
* Stops traversal gracefully when a parent folder is not accessible
* (e.g. the user entered via a "Shared with me" grant whose parent
* folder they have no permission on). In that case the partial
* breadcrumb built so far is kept — the deepest reachable ancestor
* acts as the visual root, matching how Google Drive / Dropbox handle
* shared subtrees.
*
* An error on the *target folder itself* (first iteration) is still
* treated as a real error and redirects to the home folder.
*/
async function rebuildBreadCrumb() {
2026-04-07 22:48:59 +02:00
/**
* Store the leaf (this is the current displayed folder)
2026-05-07 23:40:02 +02:00
* @type {FolderItem | null}
*/
let currentFolderInfo = null;
2026-04-07 22:48:59 +02:00
// rebuild full breadcrumb,
// TODO: to optimize, data may already be known / or ETAG could be interesting to reduce load
app.breadcrumbPath = [];
/** @type {string | null} */
let id = app.currentPath;
// recurse from selected folder to root
while (id !== null) {
console.log(`fetching folder information for folder ${id}`);
try {
2026-04-07 22:50:42 +02:00
const folderInfo = await getFolder(id);
// store the Leaf which is the current folder
if (currentFolderInfo === null) {
currentFolderInfo = folderInfo;
}
// Add every folder to the breadcrumb, including the root (home folder).
// updateBreadcrumb() no longer auto-prepends home — it's our responsibility here.
app.breadcrumbPath.unshift({
id: folderInfo.id,
name: folderInfo.name
});
// iterate to parent folder
id = folderInfo.parent_id;
2026-04-07 22:50:42 +02:00
} catch (_e) {
2026-05-22 10:15:25 +02:00
if (currentFolderInfo === null) {
// Failed on the target folder itself — real error, fall back to home.
console.warn(`Cannot access target folder ${app.currentPath}, falling back to home`);
uiNotifications.show('error: folder not found or permission denied', 'the given folder is not available or you do not have sufficient rights');
app.breadcrumbPath = [];
id = app.userHomeFolderId;
if (id) app.currentPath = id;
} else {
// Failed on a parent — hit the permission boundary of a shared subtree.
// Stop traversal; the partial breadcrumb is the best we can show.
console.log(`Stopped breadcrumb traversal at permission boundary (parent of ${currentFolderInfo.id} is not accessible)`);
break;
}
}
}
2026-04-07 22:48:59 +02:00
// store informations on the current folder
app.currentFolderInfo = currentFolderInfo;
}
// TODO split load() vs view()
/**
* Files view loading logic
2026-04-07 22:48:59 +02:00
*
* @param {Object} options
* @param {boolean} [options.insertHistory] add browser history (default true)
* @param {boolean} [options.forceRefresh] force refresh of content
2026-04-30 02:02:27 +02:00
*
*/
2026-04-07 22:48:59 +02:00
async function loadFiles(options = { insertHistory: true }) {
try {
2026-04-07 22:48:59 +02:00
console.log('Starting loadFiles() - loading files...', options);
const forceRefresh = options.forceRefresh || false;
if (isLoadingFiles) {
2026-04-07 22:48:59 +02:00
console.log('A file load is already in progress, ignoring request');
return;
}
isLoadingFiles = true;
// This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout
2026-04-07 22:50:42 +02:00
const loadingFiles = setTimeout(() => {
// display loader after few delay (will be canceled if result take less time)
ui.showError(`
<div class="files-loading-spinner">
<div class="spinner"></div>
<span>${i18n.t('files.loading')}</span>
</div>
2026-04-07 22:48:59 +02:00
`);
}, 100);
if (!app.userHomeFolderId) {
await resolveHomeFolder();
}
2026-04-07 22:50:42 +02:00
const timestamp = Math.floor(Date.now() / 1000);
await rebuildBreadCrumb();
// request a breadcrumb paint
ui.updateBreadcrumb();
updateHistory(options.insertHistory || false);
let url;
if (!app.currentPath || app.currentPath === '') {
if (app.userHomeFolderId) {
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
app.currentPath = app.userHomeFolderId;
app.breadcrumbPath = [];
ui.updateBreadcrumb();
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
} else {
url = `/api/folders?t=${timestamp}`;
2026-04-07 22:48:59 +02:00
console.warn('Emergency fallback to root folder - this should not normally happen');
}
} else {
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
console.log(`Loading subfolder content: ${app.currentPath}`);
}
/** @type {HeadersInit} */
const headers = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
2026-04-07 22:48:59 +02:00
Pragma: 'no-cache'
};
/** @type {RequestInit} */
const requestOptions = {
headers,
credentials: 'same-origin',
cache: 'no-store'
};
if (forceRefresh) {
url += `&force_refresh=true`;
2026-05-07 23:40:02 +02:00
if (requestOptions.headers) {
const headers = new Headers(requestOptions.headers);
headers.set('X-Force-Refresh', 'true');
requestOptions.headers = headers;
}
console.log('Forcing complete refresh ignoring cache');
}
console.log(`Loading listing from ${url}`);
const response = await fetch(url, requestOptions);
// not required anymore
2026-04-07 22:48:59 +02:00
clearTimeout(loadingFiles);
if (response.status === 403) {
console.warn('Forbidden when loading files');
// FIXME: i18n
ui.showError(`<p>Could not load files</p>`);
return;
}
if (!response.ok) {
throw new Error(`Server responded with status: ${response.status}`);
}
const listing = await response.json();
ui._items.clear();
ui.resetFilesList();
if (multiSelect) {
multiSelect.clear();
multiSelect.init(); // this will wire buttons & select-all-checkbox
}
2026-05-07 23:40:02 +02:00
/** @type {FolderItem[]} */
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
2026-04-30 02:02:27 +02:00
2026-05-07 23:40:02 +02:00
/** @type {FileItem[]} */
const fileList = Array.isArray(listing.files) ? listing.files : [];
if (folderList.length === 0 && fileList.length === 0) {
ui.showEmptyList();
} else {
ui.renderFolders(folderList);
ui.renderFiles(fileList);
ui.resolveOwnerCells();
// check if a file was provided
if (app.viewFile) {
let fileFound = null;
// lookup for the given fle
2026-04-07 22:50:42 +02:00
for (const file of fileList) {
if (file.id === app.viewFile) {
fileFound = file;
break;
}
}
if (fileFound) {
console.log(`file ${app.viewFile} found, calling viewer`);
await inlineViewer.openFile(fileFound);
2026-04-07 22:50:42 +02:00
} else {
// remove file
console.log(`file ${app.viewFile} not found`);
app.viewFile = null;
// correct url/history as file is not found
updateHistory(false);
}
}
}
console.log(`Loaded ${folderList.length} folders and ${fileList.length} files`);
} catch (error) {
console.error('Error loading folders:', error);
ui.showNotification('Error', 'Could not load files and folders');
} finally {
isLoadingFiles = false;
}
}
export { loadFiles };