Skip to content

Commit 3f6b1ec

Browse files
authored
feat: implement file preview (#425)
* feat: implement file preview * chore: remove unused file preview utility functions * feat: integrate DOMPurify for HTML sanitization in FilePreview component - Added `dompurify` dependency to package.json and yarn.lock. - Implemented `sanitizeHTML` utility function to sanitize HTML content. - Updated FilePreview component to use `sanitizeHTML` for setting text content safely.
1 parent e6c2808 commit 3f6b1ec

File tree

6 files changed

+3504
-3532
lines changed

6 files changed

+3504
-3532
lines changed

frontend/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"dependencies": {
1313
"@apollo/client": "^3.11.10",
1414
"@auto-drive/models": "workspace:*",
15-
"@autonomys/auto-drive": "^1.4.35",
15+
"@autonomys/auto-dag-data": "^1.5.7",
16+
"@autonomys/auto-design-system": "^1.5.7",
17+
"@autonomys/auto-drive": "^1.5.7",
1618
"@cyntler/react-doc-viewer": "^1.17.0",
1719
"@graphql-codegen/cli": "^5.0.3",
1820
"@graphql-codegen/introspection": "^4.0.3",
@@ -33,6 +35,7 @@
3335
"bytes": "^3.1.2",
3436
"clsx": "^2.1.1",
3537
"dayjs": "^1.11.13",
38+
"dompurify": "^3.2.6",
3639
"ethers": "^6.13.5",
3740
"fflate": "^0.8.2",
3841
"graphql": "^16.9.0",
@@ -63,7 +66,6 @@
6366
"zustand": "^5.0.0"
6467
},
6568
"devDependencies": {
66-
"@autonomys/auto-dag-data": "^1.0.7",
6769
"@next/eslint-plugin-next": "^15.0.2",
6870
"@types/byte-size": "^8.1.2",
6971
"@types/bytes": "^3",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useCallback, useEffect, useMemo, useState } from 'react';
2+
import { FilePreview as AutoFilePreview } from '@autonomys/auto-design-system';
3+
import {
4+
OffchainMetadata,
5+
canDisplayDirectly,
6+
needsContentParsing,
7+
processFileData,
8+
decryptFileData,
9+
} from '@autonomys/auto-dag-data';
10+
import { useNetwork } from '../../contexts/network';
11+
import { NetworkId as AutoDriveNetworkId } from '../../constants/networks';
12+
import { NetworkId as AutoUtilsNetworkId } from '@autonomys/auto-utils';
13+
import { EXTERNAL_ROUTES } from '../../constants/routes';
14+
import { sanitizeHTML } from '../../utils/sanitizeHTML';
15+
16+
export const FilePreview = ({ metadata }: { metadata: OffchainMetadata }) => {
17+
const { network } = useNetwork();
18+
const [isFilePreview, setIsFilePreview] = useState(false);
19+
const [loading, setLoading] = useState(true);
20+
const [error, setError] = useState<string | null>(null);
21+
const [isDecrypted, setIsDecrypted] = useState(false);
22+
const [decryptionError, setDecryptionError] = useState<string | null>(null);
23+
const [file, setFile] = useState<Blob | null>(null);
24+
const [textContent, setTextContent] = useState<string | null>(null);
25+
26+
const safeSetTextContent = useCallback((text: string) => {
27+
setTextContent(sanitizeHTML(text));
28+
}, []);
29+
30+
const networkId = useMemo(() => {
31+
switch (network.id) {
32+
case AutoDriveNetworkId.LOCAL:
33+
case AutoDriveNetworkId.MAINNET:
34+
return AutoUtilsNetworkId.MAINNET;
35+
case AutoDriveNetworkId.TAURUS:
36+
return AutoUtilsNetworkId.TAURUS;
37+
default:
38+
return AutoUtilsNetworkId.MAINNET;
39+
}
40+
}, [network]);
41+
42+
const gatewayUrl = EXTERNAL_ROUTES.gatewayObjectDownload(metadata.dataCid);
43+
44+
const fetchFile = useCallback(
45+
async (password?: string) => {
46+
// If file is encrypted and no password provided, don't fetch
47+
if (metadata.uploadOptions?.encryption && !password && !isDecrypted) {
48+
setIsFilePreview(false);
49+
setLoading(false);
50+
return;
51+
}
52+
53+
// For non-encrypted files that can be displayed directly,
54+
if (!metadata.uploadOptions?.encryption && canDisplayDirectly(metadata)) {
55+
setIsFilePreview(true);
56+
setFile(null);
57+
setLoading(false);
58+
return;
59+
}
60+
61+
// Encrypted files always need to be fetched and decrypted
62+
setIsFilePreview(true);
63+
setLoading(true);
64+
65+
try {
66+
const response = await fetch(gatewayUrl);
67+
if (!response.ok) {
68+
throw new Error(
69+
`Failed to fetch file: ${response.status} ${response.statusText}`,
70+
);
71+
}
72+
const blob = await response.blob();
73+
setFile(blob);
74+
// For text-based files, also read the content
75+
if (needsContentParsing(metadata)) {
76+
const text = await blob.text();
77+
safeSetTextContent(text);
78+
}
79+
// Handle decryption if needed
80+
if (metadata.uploadOptions?.encryption && password) {
81+
try {
82+
const encryptedFileData = {
83+
dataArrayBuffer: await blob.arrayBuffer(),
84+
name: metadata.name ?? '',
85+
rawData: '',
86+
uploadOptions: metadata.uploadOptions,
87+
isEncrypted: true,
88+
};
89+
const decryptedFileData = await decryptFileData(
90+
password,
91+
encryptedFileData,
92+
);
93+
const decryptedBlob = await processFileData(decryptedFileData);
94+
setFile(decryptedBlob);
95+
setIsDecrypted(true);
96+
setLoading(false);
97+
return;
98+
} catch {
99+
setDecryptionError('Invalid password or decryption failed');
100+
}
101+
}
102+
setLoading(false);
103+
} catch (error) {
104+
console.error('Error fetching file:', error);
105+
setError(
106+
error instanceof Error ? error.message : 'Failed to fetch file',
107+
);
108+
setLoading(false);
109+
}
110+
},
111+
[metadata, gatewayUrl, isDecrypted],
112+
);
113+
114+
useEffect(() => {
115+
fetchFile();
116+
}, [fetchFile]);
117+
118+
return (
119+
<AutoFilePreview
120+
metadata={metadata}
121+
network={networkId}
122+
loading={loading}
123+
file={file}
124+
error={error}
125+
decryptionError={decryptionError}
126+
isFilePreview={isFilePreview}
127+
textContent={textContent}
128+
gatewayUrl={gatewayUrl}
129+
handleDecrypt={fetchFile}
130+
/>
131+
);
132+
};

frontend/src/components/UploadedObjectInformation/index.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { ObjectShareModal } from '@/components/FileTables/common/ObjectShareModa
77
import { ObjectDeleteModal } from '@/components/FileTables/common/ObjectDeleteModal';
88
import { Loader } from 'lucide-react';
99
import { ObjectDownloadModal } from '@/components/FileTables/common/ObjectDownloadModal';
10-
import { FilePreview } from '@/components/ObjectDetails/FilePreview';
11-
import { FolderPreview } from '../ObjectDetails/FolderPreview';
10+
import { FilePreview } from '@/components/FilePreview';
1211
import {
1312
ArrowDownTrayIcon,
1413
ShareIcon,
@@ -375,11 +374,7 @@ export const UploadedObjectInformation = ({
375374
Preview
376375
</h2>
377376
<div className='overflow-hidden rounded-lg border border-gray-200 dark:border-gray-800'>
378-
{object.metadata.type === 'file' ? (
379-
<FilePreview metadata={object.metadata} />
380-
) : (
381-
<FolderPreview metadata={object.metadata} />
382-
)}
377+
<FilePreview metadata={object.metadata} />
383378
</div>
384379
</div>
385380
</div>

frontend/src/utils/sanitizeHTML.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import DOMPurify from 'dompurify';
2+
3+
export const sanitizeHTML = (html: string) => {
4+
return DOMPurify.sanitize(html, {
5+
ALLOWED_TAGS: [
6+
'p',
7+
'b',
8+
'i',
9+
'em',
10+
'strong',
11+
'a',
12+
'ul',
13+
'ol',
14+
'li',
15+
'br',
16+
'img',
17+
'pre',
18+
'code',
19+
],
20+
ALLOWED_ATTR: ['href', 'src', 'alt', 'title'],
21+
ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|data:image\/)/,
22+
});
23+
};

frontend/tsconfig.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@
1919
}
2020
],
2121
"baseUrl": "./src",
22-
"paths": { "@/*": ["*"], "gql/*": ["../gql/*"] }
22+
"paths": {
23+
"@/*": ["*"],
24+
"gql/*": ["../gql/*"],
25+
"stream-fork": ["stubs/stream-fork.ts"],
26+
"stream-fork/main.js": ["stubs/stream-fork.ts"],
27+
"stream-fork/index.js": ["stubs/stream-fork.ts"]
28+
},
29+
"target": "ES2017"
2330
},
2431
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
2532
"exclude": ["node_modules"]

0 commit comments

Comments
 (0)