s3-autoindex
Version:
Serve the contents of a S3 bucket (private or public) over HTTP
221 lines (204 loc) • 7.49 kB
text/typescript
import * as rxme from 'rxme';
import { RxHttpMatcher } from './rx-http';
// import { Response, Request } from 'express-serve-static-core';
import { Response } from './rx-http';
import * as AWS from 'aws-sdk';
import * as simqle from 'simqle';
import { ServerResponse } from 'http';
import { Config } from './parse-config';
function loopListObjects(rq: simqle.Queue, s3: AWS.S3, config: Config, mypath: string,
listObjects: rxme.Subject, marker?: string): rxme.Observable {
return rxme.Observable.create(obs => {
const lo = {
// EncodingType: 'url',
Bucket: config.s3.Bucket,
Delimiter: '/',
Prefix: mypath,
Marker: marker
};
listObjects.next(rxme.LogDebug(`s3.listObjects:Request:`, lo));
s3.listObjects(lo, (error, data) => {
if (error) {
listObjects.next(rxme.LogError('AWS:', error));
return;
}
listObjects.next(new rxme.RxMe(data));
if (!data.IsTruncated) {
listObjects.next(rxme.LogDebug(`s3.listObjects:Completed`));
listObjects.complete();
obs.complete();
} else {
obs.complete();
rq.push(loopListObjects(rq, s3, config, mypath, listObjects, data.Contents[data.Contents.length - 1].Key),
(new rxme.Subject()).passTo());
}
});
});
}
function top(config: any, prefix: string): string {
return `<html>
<head>
<title>Index of s3://${config.s3.Bucket}/${prefix}</title>
</head>
<body>
<H1>Index of s3://${config.s3.Bucket}/${prefix}</H1>
<HR>
<pre>\n`;
}
function footer(): string {
return ` </pre>
</body>
</html>`;
}
function resolvHeadObject(mypath: string, so: AWS.S3.Object, rq: simqle.Queue,
rapp: rxme.Subject, res: Response, s3: AWS.S3, config: any, cpl: rxme.Subject, obs: rxme.Observer): void {
if (!config.s3.UseMetaMtime) {
res.write(`${link(so.Key.slice(mypath.length))} ${formatDate(so.LastModified)} ${leftPad(so.Size, 16, ' ')}\n`);
cpl.stopPass(false);
cpl.complete();
obs.complete();
return;
}
const params = {
Bucket: config.s3.Bucket,
Key: so.Key
};
rq.push(rxme.Observable.create(_obs => {
s3.headObject(params, (err, headObject) => {
if (err) {
rapp.next(rxme.Msg.Error(err));
cpl.stopPass(false);
_obs.complete();
return;
}
let mtime = so.LastModified;
if (headObject.Metadata.mtime) {
mtime = new Date(parseInt(headObject.Metadata.mtime, 10) * 1000);
}
res.write([`${link(so.Key.slice(mypath.length))}`,
`${formatDate(new Date(mtime))}`,
`${leftPad(so.Size, 16, ' ')}\n`].join(' '));
cpl.stopPass(false);
_obs.complete();
});
}), (new rxme.Subject()).passTo(obs));
}
function loopDirectoryItem(mypath: string, cps: (AWS.S3.CommonPrefix | AWS.S3.Object)[], idx: number,
rq: simqle.Queue, rapp: rxme.Subject, res: ServerResponse, done: rxme.Subject,
s3: AWS.S3, config: any, now: string): void {
if (idx >= cps.length) {
done.next(rxme.Msg.Number(cps.length));
return;
}
const data = cps[idx];
if ((data as AWS.S3.CommonPrefix).Prefix) {
const so = (data as AWS.S3.CommonPrefix);
res.write(`${link(so.Prefix.slice(mypath.length))} ${now} ${leftPad('-', 16, ' ')}\n`);
loopDirectoryItem(mypath, cps, idx + 1, rq, rapp, res, done, s3, config, now);
return;
} else if ((data as AWS.S3.Object).Key) {
const cpl = (new rxme.Subject()).match(rxme.Matcher.Complete(() => {
loopDirectoryItem(mypath, cps, idx + 1, rq, rapp, res, done, s3, config, now);
return true;
})).passTo(rapp);
rq.push(rxme.Observable.create(obs => {
resolvHeadObject(mypath, (data as AWS.S3.Object), rq, rapp, res, s3, config, cpl, obs);
}), cpl);
}
// }).match((rx, cpl) => {
// // file S3.Object
// if (!rx.data.Key) { return; }
// const so = rx.data as AWS.S3.Object;
// resolvHeadObject(mypath, so, res, rq, rapp, s3, config, cpl);
// return cpl;
// }).match(rxme.Matcher.Complete(() => {
// // res.write(footer());
// // res.end();
// })).passTo();
// }
// resolvHeadObject(mypath, so: AWS.S3.Object, res: Response, rq: simqle.Queue,
// rapp: rxme.Subject, s3: AWS.S3, config: any, cpl: rxme.Subject);
}
interface Spaces {
key: string;
keyDotDot: string;
spaces: string;
}
function spaces(key: string): Spaces {
let spcs = '';
let keyDotDot = key;
if (key.length >= 50) {
keyDotDot = key.slice(0, 47) + '..>';
} else {
spcs = Array(50 - key.length).fill(' ').join('');
}
return { key: key, keyDotDot: keyDotDot, spaces: spcs };
}
function leftPad(istr: any, len: number, ch: string): string {
const str = '' + istr;
if (str.length >= len) { return str; }
return Array(len - str.length).fill(ch.slice(0, 1)).join('') + str;
}
function formatDate(a: Date): string {
return [
`${leftPad(a.getDate(), 2, '0')}-${leftPad(a.getMonth() + 1, 2, '0')}-${a.getFullYear()}`,
`${leftPad(a.getHours(), 2, '0')}:${leftPad(a.getMinutes(), 2, '0')}`].join(' ');
}
function link(fname: string): string {
const spcs = spaces(fname);
return `<a href="${spcs.key}">${spcs.keyDotDot}</a>${spcs.spaces}`;
}
export default function directoryMatcher(rq: simqle.Queue, rapp: rxme.Subject,
s3: AWS.S3, config: any): rxme.MatcherCallback {
return RxHttpMatcher((remw, sub) => {
const { req, res } = remw;
let mypath = req.url.replace(/\/+/g, '/');
// console.log(`directoryMatcher:${mypath}:${req.url}`);
if (mypath.startsWith(config.basepath)) {
mypath = mypath.substr(config.basepath.length);
}
// rapp.next(rxme.LogInfo(`[${req.path}] [${mypath}]`));
if (!mypath.endsWith('/')) {
// not a directory
return;
}
if (mypath.startsWith('/')) {
mypath = mypath.substr(1);
}
// const renderList = renderDirectoryList(mypath, res, rq, rapp, s3, config);
res.statusCode = 200;
res.setHeader('X-s3-autoindex', config.version);
res.write(top(config, mypath));
if (mypath.length > 1) {
res.write(`${link('..')} ${formatDate(new Date())} ${leftPad('-', 16, ' ')}\n`);
}
rapp.next(rxme.LogInfo('directoryMatcher:', mypath));
let doneCount = 0;
let needsDoneCount = 0;
const done = new rxme.Subject();
done.match(rxme.Matcher.Number(nr => {
doneCount += nr;
// console.log(`DoneCount:${doneCount}:${needsDoneCount}`);
if (doneCount >= needsDoneCount) {
res.write(footer());
res.end();
done.complete();
}
})).passTo();
const now = formatDate(new Date());
const listObjects = new rxme.Subject().match(rx => {
// console.log(`listObject:Match:`, config.s3.Bucket, rx.data);
// rapp.next(rxme.LogDebug(`listObject:Match:`, config.s3.Bucket, rx.data));
if (rx.data.Contents && rx.data.CommonPrefixes) {
const sloo = rx.data as AWS.S3.Types.ListObjectsOutput;
// console.log(`CommonPrefix:${JSON.stringify(sloo.CommonPrefixes)}`);
const cps = sloo.CommonPrefixes || [];
const cts = sloo.Contents || [];
needsDoneCount += cps.length + cts.length;
loopDirectoryItem(mypath, cps, 0, rq, rapp, res, done, s3, config, now);
loopDirectoryItem(mypath, cts, 0, rq, rapp, res, done, s3, config, now);
}
}).match(rxme.Matcher.Complete(() => true)).passTo(rapp);
rq.push(loopListObjects(rq, s3, config, mypath, listObjects), (new rxme.Subject()).passTo());
});
}