-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcdnController.js
More file actions
129 lines (110 loc) · 4.4 KB
/
cdnController.js
File metadata and controls
129 lines (110 loc) · 4.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { Storage } from '@google-cloud/storage';
// Initialize GCS client (uses Application Default Credentials)
const storage = new Storage();
// MIME type mapping for common file extensions
const MIME_TYPES = {
'.json': 'application/json',
'.js': 'application/javascript',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.csv': 'text/csv',
'.pdf': 'application/pdf'
};
/**
* Get MIME type from file path
*/
function getMimeType(filePath) {
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
return MIME_TYPES[ext] || 'application/octet-stream';
}
/**
* Proxy endpoint to serve files from private GCS bucket
* GET /v1/static/*
*
* This serves as a proxy for files stored in gs://httparchive/
* The request path after /v1/static/ maps directly to the GCS object path
*/
export const proxyReportsFile = async (req, res, filePath) => {
try {
const BUCKET_NAME = process.env.GCS_BUCKET_NAME || 'httparchive';
// Block access to crawls and results paths
if (filePath.startsWith('crawls/') || filePath.startsWith('results/')) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Not supported. Response size too large.' }));
return;
}
// Validate file path to prevent directory traversal
if (filePath.includes('..') || filePath.includes('//')) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid file path' }));
return;
}
// Remove leading slash if present - use path directly without base path prefix
const objectPath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
// Get the file from GCS
const bucket = storage.bucket(BUCKET_NAME);
const file = bucket.file(objectPath);
// Get file metadata for content type and caching
// This implicitly checks if the file exists, saving a network request
let metadata;
try {
[metadata] = await file.getMetadata();
} catch (error) {
if (error.code === 404) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'File not found' }));
return;
}
throw error;
}
// Determine content type
const contentType = metadata.contentType || getMimeType(objectPath);
// Set response headers
res.setHeader('Content-Type', contentType);
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.setHeader('Cache-Tag', 'bucket-proxy');
// Browser cache: 1 hour, CDN cache: 1 days
res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=86400');
if (metadata.etag) {
res.setHeader('ETag', metadata.etag);
}
// Check for conditional request (If-None-Match)
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch && metadata.etag && ifNoneMatch === metadata.etag) {
res.statusCode = 304;
res.end();
return;
}
// Stream the file content to the response
res.statusCode = 200;
// If the file is gzip-encoded in GCS, pass the compressed stream through
// directly so that Content-Length (compressed size) stays accurate.
// Without this, createReadStream() decompresses transparently while
// metadata.size still reflects the compressed size, truncating the response.
const isGzipEncoded = metadata.contentEncoding === 'gzip';
if (isGzipEncoded) {
res.setHeader('Content-Encoding', 'gzip');
}
if (metadata.size) {
res.setHeader('Content-Length', metadata.size);
}
const readStream = file.createReadStream({ decompress: !isGzipEncoded });
readStream.on('error', (err) => {
console.error('Error streaming file from GCS:', err);
if (!res.headersSent) {
res.statusCode = 500;
res.end(JSON.stringify({ error: 'Failed to read file' }));
}
});
readStream.pipe(res);
} catch (error) {
console.error('Error proxying GCS file:', error);
if (!res.headersSent) {
res.statusCode = 500;
res.end(JSON.stringify({
error: 'Server failed to respond',
details: error.message
}));
}
}
};