Initial commit: LogMaster with Viewer Collector Monitor modes
This commit is contained in:
192
dist-server/s3Source.js
Normal file
192
dist-server/s3Source.js
Normal file
@@ -0,0 +1,192 @@
|
||||
"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) {
|
||||
const path = this.config.endpoint
|
||||
? `/${this.config.bucket}/${encodeURIComponent(key)}`
|
||||
: `/${encodeURIComponent(key)}`;
|
||||
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
|
||||
Reference in New Issue
Block a user