updates
This commit is contained in:
parent
4b6d3fd991
commit
c6b4c716a8
|
|
@ -274,21 +274,21 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selected Files Preview -->
|
<!-- All Attachments Preview -->
|
||||||
<div v-if="selectedBrowserFiles.length > 0" class="mt-3">
|
<div v-if="allAttachments.length > 0" class="mt-3">
|
||||||
<div class="text-caption mb-2">Selected from browser:</div>
|
<div class="text-caption mb-2">Attachments ({{ allAttachments.length }}):</div>
|
||||||
<v-chip
|
<v-chip
|
||||||
v-for="(file, i) in selectedBrowserFiles"
|
v-for="(attachment, i) in allAttachments"
|
||||||
:key="i"
|
:key="`${attachment.type}-${i}`"
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
closable
|
closable
|
||||||
@click:close="removeBrowserFile(i)"
|
@click:close="removeAttachment(attachment)"
|
||||||
class="mr-2 mb-2"
|
class="mr-2 mb-2"
|
||||||
>
|
>
|
||||||
<v-icon start size="small">mdi-file</v-icon>
|
<v-icon start size="small">{{ attachment.type === 'uploaded' ? 'mdi-upload' : 'mdi-file' }}</v-icon>
|
||||||
{{ file.name }}
|
{{ attachment.name }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -387,6 +387,25 @@ const emailDraft = ref<{
|
||||||
attachments: []
|
attachments: []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Combined attachments from both upload and browser
|
||||||
|
const allAttachments = computed(() => {
|
||||||
|
const uploaded = emailDraft.value.attachments.map((file, index) => ({
|
||||||
|
type: 'uploaded',
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
index
|
||||||
|
}));
|
||||||
|
|
||||||
|
const browser = selectedBrowserFiles.value.map((file, index) => ({
|
||||||
|
type: 'browser',
|
||||||
|
file,
|
||||||
|
name: file.displayName || file.name,
|
||||||
|
index
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...uploaded, ...browser];
|
||||||
|
});
|
||||||
|
|
||||||
// Check for stored session on mount
|
// Check for stored session on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const storedSession = localStorage.getItem('emailSessionId');
|
const storedSession = localStorage.getItem('emailSessionId');
|
||||||
|
|
@ -444,6 +463,7 @@ const loadEmailThread = async () => {
|
||||||
const response = await $fetch<{
|
const response = await $fetch<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
emails: any[];
|
emails: any[];
|
||||||
|
threads?: any[];
|
||||||
}>('/api/email/fetch-thread', {
|
}>('/api/email/fetch-thread', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -459,6 +479,12 @@ const loadEmailThread = async () => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log('[ClientEmailSection] Successfully loaded', response.emails?.length || 0, 'emails');
|
console.log('[ClientEmailSection] Successfully loaded', response.emails?.length || 0, 'emails');
|
||||||
emailThreads.value = response.emails || [];
|
emailThreads.value = response.emails || [];
|
||||||
|
|
||||||
|
// Check if we have threads from the API response
|
||||||
|
if (response.threads) {
|
||||||
|
console.log('[ClientEmailSection] Threads available:', response.threads.length);
|
||||||
|
// For now, still use emails until we implement thread UI
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ClientEmailSection] Failed to load email thread:', error);
|
console.error('[ClientEmailSection] Failed to load email thread:', error);
|
||||||
|
|
@ -538,6 +564,8 @@ const sendEmail = async () => {
|
||||||
if (emailDraft.value.attachments && emailDraft.value.attachments.length > 0) {
|
if (emailDraft.value.attachments && emailDraft.value.attachments.length > 0) {
|
||||||
for (const file of emailDraft.value.attachments) {
|
for (const file of emailDraft.value.attachments) {
|
||||||
try {
|
try {
|
||||||
|
console.log('[ClientEmailSection] Uploading file:', file.name, 'size:', file.size);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
|
|
@ -548,6 +576,8 @@ const sendEmail = async () => {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9-]/g, '');
|
.replace(/[^a-z0-9-]/g, '');
|
||||||
|
|
||||||
|
console.log('[ClientEmailSection] Upload path:', `${username}-attachments/`, 'bucket: client-emails');
|
||||||
|
|
||||||
const uploadResponse = await $fetch<{
|
const uploadResponse = await $fetch<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -564,16 +594,26 @@ const sendEmail = async () => {
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[ClientEmailSection] Upload response:', uploadResponse);
|
||||||
|
|
||||||
if (uploadResponse.success) {
|
if (uploadResponse.success) {
|
||||||
|
const attachmentPath = uploadResponse.path || uploadResponse.fileName;
|
||||||
|
console.log('[ClientEmailSection] Successfully uploaded, path:', attachmentPath);
|
||||||
|
|
||||||
uploadedAttachments.push({
|
uploadedAttachments.push({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: uploadResponse.path
|
path: attachmentPath,
|
||||||
|
bucket: 'client-emails'
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
} catch (uploadError) {
|
console.error('[ClientEmailSection] Upload failed, response:', uploadResponse);
|
||||||
console.error('Failed to upload attachment:', uploadError);
|
|
||||||
toast.error(`Failed to upload ${file.name}`);
|
toast.error(`Failed to upload ${file.name}`);
|
||||||
}
|
}
|
||||||
|
} catch (uploadError: any) {
|
||||||
|
console.error('[ClientEmailSection] Failed to upload attachment:', file.name, uploadError);
|
||||||
|
console.error('[ClientEmailSection] Upload error details:', uploadError.data || uploadError.message);
|
||||||
|
toast.error(`Failed to upload ${file.name}: ${uploadError.data?.statusMessage || uploadError.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -622,14 +662,19 @@ const sendEmail = async () => {
|
||||||
localStorage.setItem('emailSignature', JSON.stringify(signatureConfig.value));
|
localStorage.setItem('emailSignature', JSON.stringify(signatureConfig.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to thread
|
// Add to thread with all attachments info
|
||||||
|
const allAttachmentNames = [
|
||||||
|
...emailDraft.value.attachments.map((f: File) => ({ name: f.name })),
|
||||||
|
...selectedBrowserFiles.value.map((f: any) => ({ name: f.displayName || f.name }))
|
||||||
|
];
|
||||||
|
|
||||||
emailThreads.value.unshift({
|
emailThreads.value.unshift({
|
||||||
direction: 'sent',
|
direction: 'sent',
|
||||||
to: props.interest['Email Address'],
|
to: props.interest['Email Address'],
|
||||||
subject: emailDraft.value.subject,
|
subject: emailDraft.value.subject,
|
||||||
content: emailDraft.value.content,
|
content: emailDraft.value.content,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
attachments: emailDraft.value.attachments.map((f: File) => ({ name: f.name }))
|
attachments: allAttachmentNames
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close composer and reset
|
// Close composer and reset
|
||||||
|
|
@ -661,6 +706,16 @@ const removeBrowserFile = (index: number) => {
|
||||||
selectedBrowserFiles.value = selectedBrowserFiles.value.filter((_, i) => i !== index);
|
selectedBrowserFiles.value = selectedBrowserFiles.value.filter((_, i) => i !== index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeAttachment = (attachment: any) => {
|
||||||
|
if (attachment.type === 'uploaded') {
|
||||||
|
// Remove from uploaded files
|
||||||
|
emailDraft.value.attachments = emailDraft.value.attachments.filter((_, i) => i !== attachment.index);
|
||||||
|
} else if (attachment.type === 'browser') {
|
||||||
|
// Remove from browser-selected files
|
||||||
|
selectedBrowserFiles.value = selectedBrowserFiles.value.filter((_, i) => i !== attachment.index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onCredentialsSaved = (data: { sessionId: string }) => {
|
const onCredentialsSaved = (data: { sessionId: string }) => {
|
||||||
sessionId.value = data.sessionId;
|
sessionId.value = data.sessionId;
|
||||||
localStorage.setItem('emailSessionId', data.sessionId);
|
localStorage.setItem('emailSessionId', data.sessionId);
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ interface FileItem {
|
||||||
icon: string;
|
icon: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
isFolder: boolean;
|
isFolder: boolean;
|
||||||
|
bucket?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -163,8 +164,9 @@ const loadPreview = async () => {
|
||||||
try {
|
try {
|
||||||
// For images and PDFs, fetch as blob and create object URL
|
// For images and PDFs, fetch as blob and create object URL
|
||||||
if (isImage.value || isPdf.value) {
|
if (isImage.value || isPdf.value) {
|
||||||
// Fetch the file as a blob
|
// Fetch the file as a blob, including bucket if specified
|
||||||
const response = await fetch(`/api/files/proxy-preview?fileName=${encodeURIComponent(props.file.name)}`);
|
const bucket = props.file.bucket || 'client-portal';
|
||||||
|
const response = await fetch(`/api/files/proxy-preview?fileName=${encodeURIComponent(props.file.name)}&bucket=${bucket}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch file');
|
throw new Error('Failed to fetch file');
|
||||||
|
|
@ -213,13 +215,15 @@ const downloadFile = async () => {
|
||||||
// Check if Safari (iOS or desktop)
|
// Check if Safari (iOS or desktop)
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
const bucket = props.file.bucket || 'client-portal';
|
||||||
|
|
||||||
if (isSafari) {
|
if (isSafari) {
|
||||||
// For Safari, use location.href to force proper filename handling
|
// For Safari, use location.href to force proper filename handling
|
||||||
const downloadUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`;
|
const downloadUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}&bucket=${bucket}`;
|
||||||
window.location.href = downloadUrl;
|
window.location.href = downloadUrl;
|
||||||
} else {
|
} else {
|
||||||
// For other browsers, use blob approach
|
// For other browsers, use blob approach
|
||||||
const response = await fetch(`/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}`);
|
const response = await fetch(`/api/files/proxy-download?fileName=${encodeURIComponent(props.file.name)}&bucket=${bucket}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to download file');
|
throw new Error('Failed to download file');
|
||||||
|
|
|
||||||
|
|
@ -113,17 +113,29 @@
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
v-model="selectedItems"
|
v-model="selectedItems"
|
||||||
:headers="headers"
|
:headers="selectionMode ? headersSelectionMode : headers"
|
||||||
:items="filteredFiles"
|
:items="filteredFiles"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:items-per-page="25"
|
:items-per-page="25"
|
||||||
class="elevation-0"
|
class="elevation-0"
|
||||||
show-select
|
:show-select="!selectionMode"
|
||||||
item-value="name"
|
item-value="name"
|
||||||
>
|
>
|
||||||
|
<!-- Custom checkbox for selection mode -->
|
||||||
|
<template v-if="selectionMode" v-slot:item.checkbox="{ item }">
|
||||||
|
<v-checkbox
|
||||||
|
:model-value="isSelected(item)"
|
||||||
|
@update:model-value="toggleSelection(item)"
|
||||||
|
:disabled="item.isFolder"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-slot:item.displayName="{ item }">
|
<template v-slot:item.displayName="{ item }">
|
||||||
<div
|
<div
|
||||||
class="d-flex align-center py-2 cursor-pointer"
|
class="d-flex align-center py-2"
|
||||||
|
:class="{ 'cursor-pointer': !selectionMode || item.isFolder }"
|
||||||
@click="handleFileClick(item)"
|
@click="handleFileClick(item)"
|
||||||
>
|
>
|
||||||
<v-icon :icon="item.icon" class="mr-3" :color="item.isFolder ? 'primary' : ''" />
|
<v-icon :icon="item.icon" class="mr-3" :color="item.isFolder ? 'primary' : ''" />
|
||||||
|
|
@ -338,6 +350,7 @@ interface FileItem {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
isFolder: boolean;
|
isFolder: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
bucket?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -357,6 +370,7 @@ const toast = useToast();
|
||||||
const files = ref<FileItem[]>([]);
|
const files = ref<FileItem[]>([]);
|
||||||
const filteredFiles = ref<FileItem[]>([]);
|
const filteredFiles = ref<FileItem[]>([]);
|
||||||
const selectedItems = ref<string[]>([]);
|
const selectedItems = ref<string[]>([]);
|
||||||
|
const selectedFilesInBrowser = ref<FileItem[]>([]);
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
|
|
@ -384,6 +398,15 @@ const headers = [
|
||||||
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' as const },
|
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' as const },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Table headers for selection mode
|
||||||
|
const headersSelectionMode = [
|
||||||
|
{ title: '', key: 'checkbox', sortable: false, width: '50px' },
|
||||||
|
{ title: 'Name', key: 'displayName', sortable: true },
|
||||||
|
{ title: 'Size', key: 'sizeFormatted', sortable: true },
|
||||||
|
{ title: 'Modified', key: 'lastModified', sortable: true },
|
||||||
|
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' as const },
|
||||||
|
];
|
||||||
|
|
||||||
// Breadcrumb items
|
// Breadcrumb items
|
||||||
const breadcrumbItems = computed(() => {
|
const breadcrumbItems = computed(() => {
|
||||||
const items = [
|
const items = [
|
||||||
|
|
@ -473,14 +496,35 @@ const filterFiles = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if file is selected
|
||||||
|
const isSelected = (item: FileItem) => {
|
||||||
|
return selectedFilesInBrowser.value.some(f => f.name === item.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle file selection
|
||||||
|
const toggleSelection = (item: FileItem) => {
|
||||||
|
if (item.isFolder) return;
|
||||||
|
|
||||||
|
const index = selectedFilesInBrowser.value.findIndex(f => f.name === item.name);
|
||||||
|
if (index > -1) {
|
||||||
|
selectedFilesInBrowser.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
selectedFilesInBrowser.value.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit selection event
|
||||||
|
emit('file-selected', {
|
||||||
|
...item,
|
||||||
|
path: item.name,
|
||||||
|
bucket: item.bucket || 'client-portal'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Handle file/folder click
|
// Handle file/folder click
|
||||||
const handleFileClick = (item: FileItem) => {
|
const handleFileClick = (item: FileItem) => {
|
||||||
if (props.selectionMode && !item.isFolder) {
|
if (props.selectionMode && !item.isFolder) {
|
||||||
// In selection mode, emit the file for attachment
|
// In selection mode, toggle selection on click
|
||||||
emit('file-selected', {
|
toggleSelection(item);
|
||||||
...item,
|
|
||||||
path: item.name
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -788,10 +832,22 @@ const formatDate = (date: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show selection count at the bottom in selection mode
|
||||||
|
const selectionCount = computed(() => {
|
||||||
|
return selectedFilesInBrowser.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
// Load files on mount
|
// Load files on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add selection counter at the bottom for selection mode
|
||||||
|
if (props.selectionMode) {
|
||||||
|
watch(selectionCount, (count) => {
|
||||||
|
console.log('[FileBrowser] Selection count:', count);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { getDownloadUrl } from '~/server/utils/minio';
|
import { getMinioClient } from '~/server/utils/minio';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const fileName = query.fileName as string;
|
const fileName = query.fileName as string;
|
||||||
|
const bucket = (query.bucket as string) || 'client-portal'; // Support bucket parameter
|
||||||
|
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
throw createError({
|
throw createError({
|
||||||
|
|
@ -18,10 +19,26 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
try {
|
try {
|
||||||
console.log(`[proxy-download] Attempting to download ${fileName} (attempt ${attempt + 1}/3)`);
|
console.log(`[proxy-download] Attempting to download ${fileName} from bucket ${bucket} (attempt ${attempt + 1}/3)`);
|
||||||
|
|
||||||
// Get the download URL from MinIO
|
// Get the download URL from MinIO with the correct bucket
|
||||||
const url = await getDownloadUrl(fileName);
|
const client = getMinioClient();
|
||||||
|
|
||||||
|
// Extract just the filename for the download header
|
||||||
|
let filename = fileName.split('/').pop() || fileName;
|
||||||
|
|
||||||
|
// Remove timestamp prefix if present
|
||||||
|
const timestampMatch = filename.match(/^\d{10,}-(.+)$/);
|
||||||
|
if (timestampMatch) {
|
||||||
|
filename = timestampMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate presigned URL with download headers
|
||||||
|
const responseHeaders = {
|
||||||
|
'response-content-disposition': `attachment; filename="${filename}"`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = await client.presignedGetObject(bucket, fileName, 60 * 60, responseHeaders);
|
||||||
|
|
||||||
// Fetch the file from MinIO with timeout
|
// Fetch the file from MinIO with timeout
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { getDownloadUrl } from '~/server/utils/minio';
|
import { getMinioClient } from '~/server/utils/minio';
|
||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const fileName = query.fileName as string;
|
const fileName = query.fileName as string;
|
||||||
|
const bucket = (query.bucket as string) || 'client-portal'; // Support bucket parameter
|
||||||
|
|
||||||
console.log('Proxy preview requested for:', fileName);
|
console.log('Proxy preview requested for:', fileName, 'in bucket:', bucket);
|
||||||
|
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
throw createError({
|
throw createError({
|
||||||
|
|
@ -19,9 +20,10 @@ export default defineEventHandler(async (event) => {
|
||||||
const contentType = mime.lookup(fileName) || 'application/octet-stream';
|
const contentType = mime.lookup(fileName) || 'application/octet-stream';
|
||||||
console.log('Content type:', contentType);
|
console.log('Content type:', contentType);
|
||||||
|
|
||||||
// Get the download URL
|
// Get the download URL with the correct bucket
|
||||||
const url = await getDownloadUrl(fileName);
|
const client = getMinioClient();
|
||||||
console.log('MinIO URL obtained');
|
const url = await client.presignedGetObject(bucket, fileName, 60 * 60);
|
||||||
|
console.log('MinIO URL obtained for bucket:', bucket);
|
||||||
|
|
||||||
// Fetch the file content from MinIO
|
// Fetch the file content from MinIO
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
@ -44,7 +46,7 @@ export default defineEventHandler(async (event) => {
|
||||||
setHeader(event, 'Content-Type', contentType);
|
setHeader(event, 'Content-Type', contentType);
|
||||||
setHeader(event, 'Content-Disposition', `inline; filename="${fileName.split('/').pop()}"`);
|
setHeader(event, 'Content-Disposition', `inline; filename="${fileName.split('/').pop()}"`);
|
||||||
setHeader(event, 'Cache-Control', 'public, max-age=3600');
|
setHeader(event, 'Cache-Control', 'public, max-age=3600');
|
||||||
setHeader(event, 'Content-Length', String(buffer.length));
|
setHeader(event, 'Content-Length', buffer.length);
|
||||||
|
|
||||||
// For PDF files, add additional headers
|
// For PDF files, add additional headers
|
||||||
if (contentType === 'application/pdf') {
|
if (contentType === 'application/pdf') {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue