194 lines
7.7 KiB
JavaScript
194 lines
7.7 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.S3Source = void 0;
|
|
const https = __importStar(require("https"));
|
|
const http = __importStar(require("http"));
|
|
const crypto = __importStar(require("crypto"));
|
|
const zlib = __importStar(require("zlib"));
|
|
/**
|
|
* Minimal S3 client without external dependencies.
|
|
* Supports AWS S3 and S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
|
|
*/
|
|
class S3Source {
|
|
config;
|
|
host;
|
|
useHttps;
|
|
constructor(config) {
|
|
this.config = config;
|
|
if (config.endpoint) {
|
|
const url = new URL(config.endpoint);
|
|
this.host = url.host;
|
|
this.useHttps = url.protocol === 'https:';
|
|
}
|
|
else {
|
|
this.host = `${config.bucket}.s3.${config.region}.amazonaws.com`;
|
|
this.useHttps = true;
|
|
}
|
|
}
|
|
/**
|
|
* List objects in the bucket with optional prefix
|
|
*/
|
|
async listObjects(prefix) {
|
|
const usedPrefix = prefix || this.config.prefix || '';
|
|
const queryParams = usedPrefix ? `list-type=2&prefix=${encodeURIComponent(usedPrefix)}` : 'list-type=2';
|
|
const path = this.config.endpoint ? `/${this.config.bucket}/?${queryParams}` : `/?${queryParams}`;
|
|
const response = await this.request('GET', path, '');
|
|
const objects = [];
|
|
// Simple XML parsing for S3 ListObjectsV2 response
|
|
const contentRegex = /<Contents>([\s\S]*?)<\/Contents>/g;
|
|
let match;
|
|
while ((match = contentRegex.exec(response)) !== null) {
|
|
const content = match[1];
|
|
const key = this.extractXmlValue(content, 'Key');
|
|
const size = parseInt(this.extractXmlValue(content, 'Size') || '0', 10);
|
|
const lastModified = this.extractXmlValue(content, 'LastModified') || '';
|
|
if (key && !key.endsWith('/')) {
|
|
objects.push({ key, size, lastModified });
|
|
}
|
|
}
|
|
return objects;
|
|
}
|
|
/**
|
|
* Download an object's content as string
|
|
*/
|
|
async getObject(key) {
|
|
// Encode each path segment separately to preserve /
|
|
const encodedKey = key.split('/').map(s => encodeURIComponent(s)).join('/');
|
|
const path = this.config.endpoint
|
|
? `/${this.config.bucket}/${encodedKey}`
|
|
: `/${encodedKey}`;
|
|
const response = await this.requestRaw('GET', path, '');
|
|
// Auto-decompress if gzipped
|
|
if (key.endsWith('.gz') || key.endsWith('.gzip')) {
|
|
try {
|
|
const decompressed = zlib.gunzipSync(response);
|
|
return decompressed.toString('utf-8');
|
|
}
|
|
catch {
|
|
return response.toString('utf-8');
|
|
}
|
|
}
|
|
return response.toString('utf-8');
|
|
}
|
|
extractXmlValue(xml, tag) {
|
|
const regex = new RegExp(`<${tag}>(.*?)</${tag}>`);
|
|
const match = xml.match(regex);
|
|
return match ? match[1] : null;
|
|
}
|
|
async request(method, path, body) {
|
|
const buf = await this.requestRaw(method, path, body);
|
|
return buf.toString('utf-8');
|
|
}
|
|
async requestRaw(method, path, body) {
|
|
const now = new Date();
|
|
const dateStamp = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
|
const shortDate = dateStamp.substring(0, 8);
|
|
const headers = {
|
|
'host': this.host,
|
|
'x-amz-date': dateStamp,
|
|
'x-amz-content-sha256': this.sha256(body),
|
|
};
|
|
// Create canonical request
|
|
const [pathPart, queryPart] = path.split('?');
|
|
const canonicalQueryString = queryPart || '';
|
|
const signedHeaderKeys = Object.keys(headers).sort();
|
|
const signedHeaders = signedHeaderKeys.join(';');
|
|
const canonicalHeaders = signedHeaderKeys.map(k => `${k}:${headers[k]}\n`).join('');
|
|
const canonicalRequest = [
|
|
method,
|
|
pathPart,
|
|
canonicalQueryString,
|
|
canonicalHeaders,
|
|
signedHeaders,
|
|
headers['x-amz-content-sha256'],
|
|
].join('\n');
|
|
// Create string to sign
|
|
const credentialScope = `${shortDate}/${this.config.region}/s3/aws4_request`;
|
|
const stringToSign = [
|
|
'AWS4-HMAC-SHA256',
|
|
dateStamp,
|
|
credentialScope,
|
|
this.sha256(canonicalRequest),
|
|
].join('\n');
|
|
// Calculate signature
|
|
const signingKey = this.getSignatureKey(shortDate);
|
|
const signature = this.hmac(signingKey, stringToSign).toString('hex');
|
|
// Authorization header
|
|
headers['authorization'] = `AWS4-HMAC-SHA256 Credential=${this.config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
hostname: this.host.split(':')[0],
|
|
port: this.host.includes(':') ? parseInt(this.host.split(':')[1]) : (this.useHttps ? 443 : 80),
|
|
path: path,
|
|
method: method,
|
|
headers: headers,
|
|
};
|
|
const transport = this.useHttps ? https : http;
|
|
const req = transport.request(options, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => {
|
|
const data = Buffer.concat(chunks);
|
|
if (res.statusCode && res.statusCode >= 400) {
|
|
reject(new Error(`S3 error ${res.statusCode}: ${data.toString('utf-8').substring(0, 200)}`));
|
|
}
|
|
else {
|
|
resolve(data);
|
|
}
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
req.setTimeout(30000, () => { req.destroy(); reject(new Error('S3 request timeout')); });
|
|
if (body)
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
sha256(data) {
|
|
return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
|
|
}
|
|
hmac(key, data) {
|
|
return crypto.createHmac('sha256', key).update(data, 'utf8').digest();
|
|
}
|
|
getSignatureKey(dateStamp) {
|
|
const kDate = this.hmac(`AWS4${this.config.secretAccessKey}`, dateStamp);
|
|
const kRegion = this.hmac(kDate, this.config.region);
|
|
const kService = this.hmac(kRegion, 's3');
|
|
return this.hmac(kService, 'aws4_request');
|
|
}
|
|
}
|
|
exports.S3Source = S3Source;
|
|
//# sourceMappingURL=s3Source.js.map
|