Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 1x 4x 4x 4x 4x 1x 3x 1x 1x 2x 2x 1x 1x 1x 1x 1x 1x 1x 2x 2x 1x 1x 1x 1x 1x 2x 2x 1x 1x 1x 1x 1x | const formValidator = require('./form_validator');
const photoModel = require('./photo_model');
const producer = require('./queue/producer');
const jobStore = require('./queue/job_store');
const storage = require('./storage/storage');
const zipRepository = require('./firebase/zip_repository');
const { requireAuth } = require('./firebase/auth');
const { rateLimit } = require('./rate_limit/middleware');
// Resolve the GCS object name for a set of tags. Prefer the fast in-memory job
// store (current session), then fall back to Firebase so downloads still work
// after a restart. Never throws: a database error just means "not found".
async function resolveObjectName(tags) {
const local = jobStore.getObjectName(tags);
Iif (local) {
return local;
}
try {
const record = await zipRepository.findLatestByTags(tags);
return record ? record.path : undefined;
} catch (error) {
console.error('failed to look up stored zip', error);
return undefined;
}
}
function route(app) {
app.get('/', async (req, res) => {
const tags = req.query.tags;
const tagmode = req.query.tagmode;
const ejsLocalVariables = {
tagsParameter: tags || '',
tagmodeParameter: tagmode || '',
photos: [],
searchResults: false,
invalidParameters: false,
// Part V - whether a zip for these tags is already available (so the page
// can show the download button straight away after a reload).
zipReady: false
};
// if no input params are passed in then render the view with out querying the api
if (!tags && !tagmode) {
return res.render('index', ejsLocalVariables);
}
// validate query parameters
if (!formValidator.hasValidFlickrAPIParams(tags, tagmode)) {
ejsLocalVariables.invalidParameters = true;
return res.render('index', ejsLocalVariables);
}
// Part V - if the zip for these tags is already available (in memory or
// persisted in Firebase), let the view show the download button right away.
ejsLocalVariables.zipReady = Boolean(await resolveObjectName(tags));
// get photos from flickr public feed api
return photoModel
.getFlickrPhotos(tags, tagmode)
.then(photos => {
ejsLocalVariables.photos = photos;
ejsLocalVariables.searchResults = true;
return res.render('index', ejsLocalVariables);
})
.catch(error => {
console.log('aspdfonaposd', error);
return res.status(500).send({ error });
});
});
// Part I & II - endpoint producer: queue a "zip these tags" job.
// Called over AJAX from the search results page; returns JSON so the page can
// start polling /zip/status instead of navigating away to the raw response.
// Protected by two layers, applied before any work is done:
// - rateLimit: token-bucket throttling per client IP (Memory Store TP) so a
// malicious actor can't spam the zip-generation button. Runs first so it
// shields even the (relatively costly) auth verification from abuse.
// - requireAuth: a valid Firebase ID token (Google Sign-In).
app.post('/zip', rateLimit, requireAuth, async (req, res) => {
const tags = req.query.tags;
const tagmode = req.query.tagmode || 'all';
if (!tags) {
return res.status(400).send({ error: 'Missing "tags" query parameter' });
}
try {
const messageId = await producer.publishZipJob(tags, tagmode);
// Record the freshly queued job so /zip/status reports "queued" until the
// worker picks it up.
jobStore.markQueued(tags);
return res.status(202).send({ status: 'queued', messageId, tags });
} catch (error) {
console.error('failed to publish zip job', error);
return res.status(500).send({ error: 'Failed to queue zip job' });
}
});
// Live status endpoint polled by the progress bar on the page.
app.get('/zip/status', (req, res) => {
const tags = req.query.tags;
if (!tags) {
return res.status(400).send({ error: 'Missing "tags" query parameter' });
}
const status = jobStore.getStatus(tags);
return res.status(200).send({
status: status.status,
progress: status.progress,
// Once ready, tell the client where to stream the file from.
downloadUrl: status.status === 'done'
? `/zip/download?tags=${encodeURIComponent(tags)}`
: null,
error: status.error
});
});
// Part II - list every zip already generated, read straight from Firebase.
// The client fetches this to show the "previously generated zips" section.
app.get('/zips', async (req, res) => {
try {
const zips = await zipRepository.listZips();
return res.status(200).send({ zips });
} catch (error) {
console.error('failed to list zips', error);
return res.status(500).send({ error: 'Failed to list zips' });
}
});
// Part V - stream the finished zip straight from Google Cloud Storage to the
// client (Content-Disposition: attachment triggers the browser download).
app.get('/zip/download', async (req, res) => {
const tags = req.query.tags;
if (!tags) {
return res.status(400).send({ error: 'Missing "tags" query parameter' });
}
// Resolve from memory first, then Firebase (so downloads survive a restart).
const objectName = await resolveObjectName(tags);
Eif (!objectName) {
return res.status(404).send({ error: 'No zip available for these tags yet' });
}
const safeName = String(tags).trim().replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '');
const filename = `${safeName || 'photos'}.zip`;
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
const readStream = storage.createReadStream(objectName);
readStream.on('error', error => {
console.error('failed to stream zip', error);
if (!res.headersSent) {
res.status(500).send({ error: 'Failed to download zip' });
} else {
res.destroy(error);
}
});
return readStream.pipe(res);
});
}
module.exports = route;
|