Fix bugs: office auth, PDF worker, sidebar pins, direct downloads
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m0s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m0s
This commit is contained in:
parent
e88243f097
commit
bfe55e2bfc
6 changed files with 74 additions and 32 deletions
|
|
@ -1026,7 +1026,12 @@ func (h *FSHandler) Download(c *fiber.Ctx) error {
|
||||||
// Set appropriate headers for range requests
|
// Set appropriate headers for range requests
|
||||||
c.Set("Accept-Ranges", "bytes")
|
c.Set("Accept-Ranges", "bytes")
|
||||||
c.Set("Content-Type", mime.TypeByExtension(filepath.Ext(resolvedPath)))
|
c.Set("Content-Type", mime.TypeByExtension(filepath.Ext(resolvedPath)))
|
||||||
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.Name()))
|
|
||||||
|
if c.Query("download") == "true" {
|
||||||
|
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.Name()))
|
||||||
|
} else {
|
||||||
|
c.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", info.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
// Use Fiber's built-in SendFile which supports Range requests
|
// Use Fiber's built-in SendFile which supports Range requests
|
||||||
return c.SendFile(resolvedPath)
|
return c.SendFile(resolvedPath)
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ func GenerateEditToken(userID int, username, secret, filePath string) (string, e
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"sub": userID,
|
"sub": userID,
|
||||||
"username": username,
|
"username": username,
|
||||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||||
"iat": time.Now().Unix(),
|
"iat": time.Now().Unix(),
|
||||||
"type": "edit",
|
"type": "edit",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1395,7 +1395,27 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
<div className="w-4 h-4 flex items-center justify-center flex-shrink-0 [&>svg]:w-4 [&>svg]:h-4">
|
<div className="w-4 h-4 flex items-center justify-center flex-shrink-0 [&>svg]:w-4 [&>svg]:h-4">
|
||||||
{getFileIcon(file)}
|
{getFileIcon(file)}
|
||||||
</div>
|
</div>
|
||||||
<span className="truncate flex-1" title={getFileDisplayName(file.name, file.is_dir)}>{getFileDisplayName(file.name, file.is_dir)}</span>
|
{renaming === file.path ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleRename(file.path, file.name, file.is_dir);
|
||||||
|
if (e.key === 'Escape') setRenaming(null);
|
||||||
|
}}
|
||||||
|
onBlur={() => handleRename(file.path, file.name, file.is_dir)}
|
||||||
|
autoFocus
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="flex-1 text-sm bg-transparent outline-none w-full"
|
||||||
|
style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="truncate flex-1" title={getFileDisplayName(file.name, file.is_dir)}>
|
||||||
|
{getFileDisplayName(file.name, file.is_dir)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
@ -2274,23 +2294,27 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
className="p-2 hover:bg-white/5 rounded-lg transition-colors flex items-center gap-2 text-sm"
|
className="p-2 hover:bg-white/5 rounded-lg transition-colors flex items-center gap-2 text-sm"
|
||||||
style={{ color: 'var(--color-text-primary)' }}
|
style={{ color: 'var(--color-text-primary)' }}
|
||||||
title="Download Selected"
|
title="Download Selected"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (!downloadToken) return;
|
try {
|
||||||
const form = document.createElement('form');
|
const token = await api.createDownloadToken();
|
||||||
form.method = 'POST';
|
const form = document.createElement('form');
|
||||||
form.action = api.getBulkDownloadUrl(downloadToken);
|
form.method = 'POST';
|
||||||
form.target = '_blank';
|
form.action = api.getBulkDownloadUrl(token);
|
||||||
|
form.target = '_blank';
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'hidden';
|
const input = document.createElement('input');
|
||||||
input.name = 'paths';
|
input.type = 'hidden';
|
||||||
input.value = JSON.stringify(Array.from(selectedFiles));
|
input.name = 'paths';
|
||||||
form.appendChild(input);
|
input.value = JSON.stringify(Array.from(selectedFiles));
|
||||||
|
form.appendChild(input);
|
||||||
document.body.appendChild(form);
|
|
||||||
form.submit();
|
document.body.appendChild(form);
|
||||||
document.body.removeChild(form);
|
form.submit();
|
||||||
setSelectedFiles(new Set());
|
document.body.removeChild(form);
|
||||||
|
setSelectedFiles(new Set());
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to download selected files", err);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
|
|
@ -2385,20 +2409,28 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
icon={<Download className="w-4 h-4" />}
|
icon={<Download className="w-4 h-4" />}
|
||||||
label="Download Folder"
|
label="Download Folder"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (!downloadToken) return;
|
try {
|
||||||
window.location.href = api.getDownloadFolderUrl(contextMenu.file.path, downloadToken);
|
const token = await api.createDownloadToken();
|
||||||
setContextMenu(null);
|
window.location.href = api.getDownloadFolderUrl(contextMenu.file.path, token);
|
||||||
|
setContextMenu(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to download folder", err);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
icon={<Download className="w-4 h-4" />}
|
icon={<Download className="w-4 h-4" />}
|
||||||
label="Download"
|
label="Download"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (!downloadToken) return;
|
try {
|
||||||
window.location.href = api.getDownloadUrl(contextMenu.file.path, downloadToken);
|
const token = await api.createDownloadToken();
|
||||||
setContextMenu(null);
|
window.location.href = api.getDownloadUrl(contextMenu.file.path, token, true);
|
||||||
|
setContextMenu(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to download file", err);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
||||||
|
|
||||||
const previewType = getPreviewType(file);
|
const previewType = getPreviewType(file);
|
||||||
const downloadUrl = downloadToken ? api.getDownloadUrl(file.path, downloadToken) : '';
|
const downloadUrl = downloadToken ? api.getDownloadUrl(file.path, downloadToken) : '';
|
||||||
|
const forceDownloadUrl = downloadToken ? api.getDownloadUrl(file.path, downloadToken, true) : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|
@ -103,7 +104,7 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<a
|
<a
|
||||||
href={downloadUrl}
|
href={forceDownloadUrl}
|
||||||
download={file.name}
|
download={file.name}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||||
import 'react-pdf/dist/Page/TextLayer.css';
|
import 'react-pdf/dist/Page/TextLayer.css';
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.mjs`;
|
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PdfViewer({ url }: { url: string }) {
|
export default function PdfViewer({ url }: { url: string }) {
|
||||||
|
|
@ -53,6 +53,7 @@ export default function PdfViewer({ url }: { url: string }) {
|
||||||
<Document
|
<Document
|
||||||
file={url}
|
file={url}
|
||||||
onLoadSuccess={onDocumentLoadSuccess}
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
|
onLoadError={(error) => console.error('Failed to load PDF:', error)}
|
||||||
loading={
|
loading={
|
||||||
<div className="flex items-center justify-center h-full mt-20">
|
<div className="flex items-center justify-center h-full mt-20">
|
||||||
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-accent)' }} />
|
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-accent)' }} />
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ class ApiClient {
|
||||||
|
|
||||||
if (!retryResponse.ok && retryResponse.status === 401) {
|
if (!retryResponse.ok && retryResponse.status === 401) {
|
||||||
if (typeof window !== 'undefined') window.dispatchEvent(new Event('auth_error'));
|
if (typeof window !== 'undefined') window.dispatchEvent(new Event('auth_error'));
|
||||||
|
this.clearTokens();
|
||||||
throw new Error(`HTTP 401`);
|
throw new Error(`HTTP 401`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +91,7 @@ class ApiClient {
|
||||||
if (!response.ok && endpoint !== '/api/auth/refresh') {
|
if (!response.ok && endpoint !== '/api/auth/refresh') {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
if (typeof window !== 'undefined') window.dispatchEvent(new Event('auth_error'));
|
if (typeof window !== 'undefined') window.dispatchEvent(new Event('auth_error'));
|
||||||
|
this.clearTokens();
|
||||||
}
|
}
|
||||||
let errorMsg = `HTTP ${response.status}`;
|
let errorMsg = `HTTP ${response.status}`;
|
||||||
try {
|
try {
|
||||||
|
|
@ -318,8 +320,9 @@ class ApiClient {
|
||||||
return data.token;
|
return data.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDownloadUrl(path: string, token: string) {
|
getDownloadUrl(path: string, token: string, forceDownload: boolean = false) {
|
||||||
return `${API_BASE}/api/files/download?path=${encodeURIComponent(path)}&token=${token}`;
|
const url = `${API_BASE}/api/files/download?path=${encodeURIComponent(path)}&token=${token}`;
|
||||||
|
return forceDownload ? `${url}&download=true` : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDownloadFolderUrl(path: string, token: string) {
|
getDownloadFolderUrl(path: string, token: string) {
|
||||||
|
|
|
||||||
Reference in a new issue