All files / app route.js

60.81% Statements 45/74
60% Branches 18/30
70% Functions 7/10
60.81% Lines 45/74

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 1681x 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;