makeomatic/mservice-calendar

View on GitHub
bin/tags.js

Summary

Maintainability
A
2 hrs
Test Coverage
#!/usr/bin/env node

/* eslint-disable no-console */

const Promise = require('bluebird');
const glob = require('glob');
const path = require('path');
const request = require('request-promise');
const hashFiles = require('hash-files');
const assert = require('assert');
const flatten = require('lodash/flatten');
const mime = require('mime-types');

// base64 1x1 px transparent png
const req = request.defaults({
  headers: {
    accept: 'application/vnd.api+json',
    'content-type': 'application/vnd.api+json',
    'accept-version': '~1',
  },
  gzip: true,
  json: true,
});

// gets JWT token for CURL requests generator
const authenticate = ({ username, password, endpoint }) => (
  req
    .post({
      baseUrl: endpoint,
      uri: '/users/login',
      body: { data: { id: username, type: 'user', attributes: { password } } },
    })
    .promise()
    .get('meta')
    .get('jwt')
);

const getUploadTagsList = (argv) => {
  const root = path.resolve(process.cwd(), argv.dir);
  const sections = glob.sync('*', { cwd: root });

  // gives complete list of section/genres
  // we need to retrieve cover & icon if it's available
  return flatten(sections.map((section) => {
    const base = path.resolve(root, section);
    return glob.sync('*', { cwd: base }).map((genre) => {
      const folder = path.resolve(base, genre);
      const assetsPaths = {};
      const assetsLinks = {};

      // at this point we check that png exist
      const files = glob.sync('*.{png,jpg}', { cwd: folder });
      const id = genre.replace(/[^a-zA-Z0-9]/g, '-');

      files.forEach((asset) => {
        const ext = path.extname(asset);
        const assetName = path.basename(asset, ext);
        const assetPath = path.resolve(folder, asset);
        const hash = hashFiles.sync({ files: [assetPath], algorithm: 'sha256' });
        assetsPaths[assetName] = path.resolve(folder, asset);
        assetsLinks[assetName] = `https://${argv.cdn}/${argv.prefix}/${id}/${assetName}.${hash.slice(0, 8)}${ext}`;
      });

      // verify that cover exists
      assert.ok(assetsPaths.cover, `cover must be present for ${folder}`);

      // icon can be defaults to placeholder
      if (!assetsLinks.icon) {
        assetsLinks.icon = argv.placeholder;
      }

      return {
        assetsPaths,
        tag: {
          section,
          cover: assetsLinks.cover,
          icon: assetsLinks.icon,
          id,
          eng: genre
            .replace(/-/g, ' ')
            .replace(/_/g, '/')
            .replace(/\^/g, '&')
            .replace(/\w+/g, (match) => {
              return match[0].toUpperCase() + match.slice(1);
            }),
        },
      };
    });
  }));
};

// uploads images to CDN & prints curl requests to perform adding the data
// to the calendar
function prepareCURL(argv) {
  const genres = getUploadTagsList(argv);

  return authenticate(argv).then((token) => {
    // now generate CURL requests
    const curlRequests = genres.map(({ tag }) => (
      `curl -X POST -H 'Content-Type: application/vnd.api+json' \\
        '${argv.endpoint}/calendar/event/tags/${argv.action}' \\
        -d '${JSON.stringify({ token, tag: argv.action === 'delete' ? tag.id : tag })}'`
    ));

    console.log(`\n\nCURL ${argv.action} requests:\n----\n`);
    console.log(curlRequests.join('\n\n'));
    console.log('\n----\n\n');

    return curlRequests;
  });
}

// uploads files in the directory
function uploadFiles(argv) {
  // example:
  //  -x ~/projects/rfx-chef/cookbooks/rfx-docker/files/default/gce-key.json
  // eslint-disable-next-line import/no-dynamic-require
  const credentials = require(argv.credentials);

  const gcs = require('@google-cloud/storage')({
    projectId: credentials.projectId,
    credentials: {
      client_email: credentials.client_email,
      private_key: credentials.private_key,
    },
  });

  const genres = getUploadTagsList(argv);
  const bucket = Promise.promisifyAll(gcs.bucket(argv.cdn));

  return Promise.map(genres, ({ assetsPaths, tag }) => {
    const promises = [];

    // eslint-disable-next-line no-restricted-syntax, guard-for-in
    for (const asset in assetsPaths) {
      const assetPath = assetsPaths[asset];
      const file = path.basename(tag[asset]);

      const opts = {
        destination: `${argv.prefix}/${tag.id}/${file}`,
        resumable: false,
        public: true,
        metadata: {
          contentType: mime.lookup(assetPath) || 'application/octet-stream',
          cacheControl: 'public, max-age=31536000',
        },
      };

      promises.push(
        bucket.uploadAsync(assetPath, opts).tap(() => {
          console.info('uploaded %s to https://%s/%s', assetPath, argv.cdn, opts.destination);
        })
      );
    }

    return Promise.all(promises);
  });
}

// run program
// eslint-disable-next-line no-unused-expressions
require('yargs')
  .command({
    command: 'curl',
    desc: 'prepare curl requests based on dir structure',
    handler: prepareCURL,
  })
  .command({
    command: 'upload',
    desc: 'upload assets based on dir structure',
    handler: uploadFiles,
  })
  .option('action', {
    alias: 'a',
    describe: 'action to perform',
    choices: ['create', 'update', 'delete'],
    default: 'create',
  })
  .option('username', {
    alias: 'u',
    demandOption: true,
    describe: 'login for the users service',
  })
  .option('password', {
    alias: 'P',
    demandOption: true,
    describe: 'password for login',
  })
  .option('endpoint', {
    alias: 'h',
    demandOption: true,
    describe: 'host where API is located',
  })
  .option('credentials', {
    alias: 'x',
    demandOption: true,
    describe: 'credentials for cdn',
  })
  .option('dir', {
    alias: 'd',
    demandOption: true,
    describe: 'directory structured for tags',
  })
  .option('cdn', {
    alias: 'c',
    demandOption: true,
    describe: 'cdn to upload data to',
  })
  .option('prefix', {
    describe: 'cdn prefix',
    default: 'calendar',
  })
  .option('placeholder', {
    alias: 'p',
    demandOption: true,
    describe: 'placeholder for missing icons',
  })
  .demandCommand(1, 'You need at least one command before moving on')
  .help()
  .argv;