Listing markdown files - node.js

So I decided to redo a blog and am using markdown, I have three markdown files in a folder called blog and was wanting to list them in order by date. Problem is Im not sure what I did to screw up my array.
Heres my routes.js file
exports.list = function(req, res){
var walk = require('walk'), fs = require('fs'), options, walker;
var walker = walk.walk('blog');
var fs = new Array();
walker.on("file", function(root,file,next){
var f = root + "/" + file['name'].substring(0, file['name'].lastIndexOf('.'));
// push without /blog prefix
if (file['name'].substr(-2) == "md") {
fs.push(f.substring(f.indexOf('/')));
console.log(fs);
}
next();
});
walker.on("end", function() {
var model = {
layout:'home.hbs',
title: 'Entries',
files: fs
};
res.render('home.hbs', model)
});
};
But what I return in terminal is this:
[ '/first' ]
[ '/first', '/second' ]
[ '/first', '/second', '/third' ]
Say I just wanted to display the first two and have them sorted by date in a markdown file, like so:
Title: Lorem Ipsum dolor sit amet
Date: January 2d, 2012
# test message
Whats wrong with my array/rest of code

First thing I noticed is redeclaring of fs variable. In line 2 it's Node's Filesystem module, in line 4 it's new Array() (which should be [] if you ask me).
I'm also not sure what is walker module for and, since it's github repo was removed and npm package is outdated, I recommend you use raw filesystem module API to list files, probably path module to handle your files locations and some async to glue it together:
// `npm install async` first.
// https://github.com/caolan/async
var fs = require('fs');
var async = require('async');
// Lists directory entries in #dir,
// filters those which names ends with #extension.
// calls callback with (err, array of strings).
//
// FIXME:
// Mathes directories - solution is fs.stat()
// (could also sort here by mtime).
function listFiles(dir, extension, callback) {
fs.readdir(dir, function(err, files) {
if (err) {
console.error('Failed to list files in `%s`: %s', dir, err);
return callback(err);
}
var slicePos = -extension.length;
callback(null, files.filter(function(filename) {
return (extension == filename.slice(slicePos))
}));
});
}
// Sorts markdown based on date entry.
// Should be based on `mtime`, I think,
// since reading whole file isn't great idea.
// (See fs.Stats.)
// At lease add caching or something, you'll figure :)
//
// Also, you better get yourself a nice markdown parser,
// but for brevity I assume that first 2 lines are:
// Title: Some Title
// Date: Valid Javascript Datestring
function sortMarkdown(pathes, callback) {
async.sortBy(pathes, function(fileName, cb) {
// Following line is dirty!
// You should probably pass absolute pathes here
// to avoid errors. Path and Fs modules are your friends.
var md = __dirname + '/blogs/' + fileName;
fs.readFile(md, 'utf8', function(err, markdown) {
if (err) {
console.error('Failed to read `%s`: %s', md, err);
return cb(err);
}
// Get second line of md.
var date = markdown.split('\n')[1];
// Get datestring with whitespaces removed.
date = date.split(':')[1].trim();
// Get timestamp. Change with -ts
// to reverse sorting order.
var ts = Date.parse(date);
// Tell async how to sort; for details, see:
// https://github.com/caolan/async#sortBy
cb(null, ts);
});
}, callback);
}
function listSortedMarkdown(dir, callback) {
// Async is just great!
// https://github.com/caolan/async#waterfall
async.waterfall([
async.apply(listFiles, dir, '.md'),
sortMarkdown,
], callback);
}
listSortedMarkdown(__dirname + '/blogs', function(err, sorted) {
return err ? console.error('Error: %s', err)
: console.dir(sorted);
});

Related

Node.js function not running in order. Error: Unhandled stream error in pipe

I updated the function to create the CSV file but now I'm getting an error:
In upload function
internal/streams/legacy.js:57
throw er; // Unhandled stream error in pipe.
^
Error: ENOENT: no such file or directory, open 'C:\Users\shiv\WebstormProjects\slackAPIProject\billingData\CSV\1548963844106output.csv'
var csvFilePath = '';
var JSONFilePath = '';
function sendBillingData(){
var message = '';
axios.get(url, {
params: {
token: myToken
}
}).then(function (response) {
message = response.data;
fields = billingDataFields;
// saveFiles(message, fields, 'billingData/');
saveFilesNew(message, fields, 'billingData/');
var file = fs.createReadStream(__dirname + '/' + csvFilePath); // <--make sure this path is correct
console.log(__dirname + '/' + csvFilePath);
uploadFile(file);
})
.catch(function (error) {
console.log(error);
});
}
The saveFilesNew function is:
function saveFilesNew(message, options, folder){
try {
const passedData = message;
var relevantData='';
if (folder == 'accessLogs/'){
const loginsJSON = message.logins;
relevantData = loginsJSON;
console.log(loginsJSON);
}
if(folder == 'billingData/'){
relevantData = passedData.members;
const profile = passedData.members[0].profile;
}
//Save JSON to the output folder
var date = Date.now();
var directoryPath = folder + 'JSON/' + date + "output";
JSONFilePath = directoryPath + '.json';
fs.writeFileSync(JSONFilePath, JSON.stringify(message, null, 4), function(err) {
if (err) {
console.log(err);
}
});
//parse JSON onto the CSV
const json2csvParser = new Json2csvParser({ fields });
const csv = json2csvParser.parse(relevantData);
// console.log(csv);
//function to process the CSV onto the file
var directoryPath = folder + 'CSV/' + date + "output";
csvFilePath = directoryPath + '.csv';
let data = [];
let columns = {
real_name: 'real_name',
display_name: 'display_name',
email: 'email',
account_type: 'account_type'
};
var id = passedData.members[0].real_name;
console.log(id);
console.log("messageLength is" +Object.keys(message.members).length);
for (var i = 0; i < Object.keys(message.members).length; i++) {
console.log("value of i is" + i);
var display_name = passedData.members[i].profile.display_name;
var real_name = passedData.members[i].profile.real_name_normalized;
var email = passedData.members[i].profile.email;
var account_type = 'undefined';
console.log("name: " + real_name);
if(passedData.members[i].is_owner){
account_type = 'Org Owner';
}
else if(passedData.members[i].is_admin){
account_type = 'Org Admin';
}
else if(passedData.members[i].is_bot){
account_type = 'Bot'
}
else account_type = 'User';
data.push([real_name, display_name, email, account_type]);
}
console.log(data);
stringify(data, { header: true, columns: columns }, (err, output) => {
if (err) throw err;
fs.writeFileSync(csvFilePath, output, function(err) {
console.log(output);
if (err) {
console.log(err);
}
console.log('my.csv saved.');
});
});
} catch (err) {
console.error(err);
}
}
The upload file function is:
function uploadFile(file){
console.log("In upload function");
const form = new FormData();
form.append('token', botToken);
form.append('channels', 'testing');
form.append('file', file);
axios.post('https://slack.com/api/files.upload', form, {
headers: form.getHeaders()
}).then(function (response) {
var serverMessage = response.data;
console.log(serverMessage);
});
}
So I think the error is getting caused because node is trying to upload the file before its being created. I feel like this has something to do with the asynchronous nature of Node.js but I fail to comprehend how to rectify the code. Please let me know how to correct this and mention any improvements to the code structure/design too.
Thanks!
You don't wait for the callback provided to stringify to be executed, and it's where you create the file. (Assuming this stringify function really does acccept a callback.)
Using callbacks (you can make this cleaner with promises and these neat async/await controls, but let's just stick to callbacks here), it should be more like:
function sendBillingData() {
...
// this callback we'll use to know when the file writing is done, and to get the file path
saveFilesNew(message, fields, 'billingData/', function(err, csvFilePathArgument) {
// this we will execute when saveFilesNew calls it, not when saveFilesNew returns, see below
uploadFile(fs.createReadStream(__dirname + '/' + csvFilePathArgument))
});
}
// let's name this callback... "callback".
function saveFilesNew(message, options, folder, callback) {
...
var csvFilePath = ...; // local variable only instead of your global
...
stringify(data, { header: true, columns: columns }, (err, output) => {
if (err) throw err; // or return callbcack(err);
fs.writeFile(csvFilePath , output, function(err) { // NOT writeFileSync, or no callback needed
console.log(output);
if (err) {
console.log(err);
// callback(err); may be a useful approach for error-handling at a higher level
}
console.log('my.csv saved.'); // yes, NOW the CSV is saved, not before this executes! Hence:
callback(null, csvFilePath); // no error, clean process, pass the file path
});
});
console.log("This line is executed before stringify's callback is called!");
return; // implicitly, yes, yet still synchronous and that's why your version crashes
}
Using callbacks that are called only when the expected events happen (a file is done writing, a buffer/string is done transforming...) allows JS to keep executing code in the meantime. And it does keep executing code, so when you need data from an async code, you need to tell JS you need it done before executing your piece.
Also, since you can pass data when calling back (it's just a function), here I could avoid relying on a global csvFilePath. Using higher level variables makes things monolithic, like you could not transfer saveFilesNew to a dedicated file where you keep your toolkit of file-related functions.
Finally, if your global process is like:
function aDayAtTheOffice() {
sendBillingData();
getCoffee();
}
then you don't need to wait for the billing data to be processed before starting making coffee. However, if your boss told you that you could NOT get a coffee until the billing data was settled, then your process would look like:
function aDayAtTheOffice() {
sendBillingData(function (err) {
// if (err) let's do nothing here: you wanted a coffee anyway, right?
getCoffee();
});
}
(Note that callbacks having potential error as first arg and data as second arg is a convention, nothing mandatory.)
IMHO you should read about scope (the argument callback could be accessed at a time where the call to saveFilesNew was already done and forgotten!), and about the asynchronous nature of No... JavaScript. ;) (Sorry, probably not the best links but they contain the meaningful keywords, and then Google is your buddy, your friend, your Big Brother.)

Sending response after forEach

(Please note this is not a duplicate of two similarly titled questions, those two questions use Mongoose and the answers apply to Mongoose queries only)
I have a list of directories, each of these directories contains a file. I want to return a JSON list with the contents of each of these files. I can load the files no problem, but because I'm looping over the array with forEach, my empty response is sent before I've actually loaded the contents of the files:
function getInputDirectories() {
return fs.readdirSync(src_path).filter(function(file) {
return fs.statSync(path.join(src_path, file)).isDirectory();
});
}
router.get('/list', function(req, res, next) {
var modules = [];
var input_dirs = getInputDirectories();
input_dirs.forEach(function(dir) {
path = __dirname+'/../../modules/input/'+dir+'/module.json'
fs.readFile(path, 'utf8', function(err, data) {
modules.push(data);
});
});
res.status(200).json(modules);
});
How can I make sure that I only send down the modules array once it's fully loaded, ie: once the forEach is done.
Since fs.readFile is asynchronous, the behaviour that you are having is most likely the expected one.
What you need to do is return your modules when all modules have been read. You could do this inside fs.readFile.
As far as I have understood, you can obtain the total number of directories through input_dirs.length (since I guess getInputDirectories() is returning an array). Now you need some kind of a counter that helps you understand if you have read the last directory or not, and if yes, then you return your modules. Something like this should work:
router.get('/list', function(req, res, next) {
var modules = [];
var input_dirs = getInputDirectories();
var c = 0;
input_dirs.forEach(function(dir) {
path = __dirname+'/../../modules/input/' + dir + '/module.json'
fs.readFile(path, 'utf8', function(err, data) {
c++;
modules.push(data);
if(c == input_dirs.length) {
return res.status(200).json(modules);
}
});
});
});
I suggest you use Promises, example:
var Promise = require('bluebird');
router.get('/list', function(req, res, next) {
var modules = [];
var input_dirs = getInputDirectories();
// 'each' will try to fulfill all promises, if one fails, it returns a
// failed promise.
return Promise.each(input_dirs, function(dir){
path = __dirname+'/../../modules/input/'+dir+'/module.json';
return new Promise(function(resolve, reject){
fs.readFile(path, 'utf8', function(err, data) {
if (err) return reject(err);
return resolve(data);
});
});
}).then(function(modules){
return res.status(200).json(modules);
})
.catch(function(err){
if (err) {
//handle error
}
});
});
This way you move one once you fulfilled your promises.
Instead of fs.readFile use fs.readFileSync

Listing all the directories and all the files and uploading them to my bucket (S3 Amazon) with Node.JS

Code below:
I'm using the findit walker, documentation here -> https://github.com/substack/node-findit
With this package i'm listing all the directories and files of my application, and i'm trying to send to my bucket on Amazon S3 (with my own code).
I'm not sure if the code is right, and i don't know what i need to put in the Body, inside the params object.
This part it's listening all the Directories of my app:
finder.on('directory', function (dir, stat, stop) {
var base = path.basename(dir);
if (base === '.git' || base === 'node_modules' || base === 'bower_components') {
stop();
}
else {
console.log(dir + '/');
}
});
And this one it's listening all the files of my app:
finder.on('file', function (file, stat) {
console.log(file);
});
I updated it to send data to my bucket, like this:
finder.on('file', function (file, stat) {
console.log(file);
var params = {
Bucket: BUCKET_NAME,
Key: file,
//Body:
};
//console.log(params.body);
s3.putObject(params, function(err) {
if(err) {
console.log(err);
}
else {
console.log("Success!");
}
});
});
I really don't know what i need to put inside the Body, and i don't know if the code is right. Anyone could help me?
Thanks.
to help, all code, all the code:
var fs = require('fs');
var finder = require('findit')(process.argv[2] || '.');
var path = require('path');
var aws = require('aws-sdk');
var s3 = new aws.S3();
aws.config.loadFromPath('./AwsConfig.json');
var BUCKET_NAME = 'test-dev-2';
finder.on('directory', function (dir, stat, stop) {
var base = path.basename(dir);
if (base === '.git' || base === 'node_modules' || base === 'bower_components') {
stop();
}
else {
console.log(dir + '/');
}
});
finder.on('file', function (file, stat) {
console.log(file);
var params = {
Bucket: BUCKET_NAME,
Key: file,
//Body:
};
//console.log(params.body);
s3.putObject(params, function(err) {
if(err) {
console.log(err);
}
else {
console.log("Success");
}
});
});
finder.on('error', function (err) {
console.log(err);
});
finder.on('end', function () {
console.log('Done!');
});
Based on the documentation, the Body parameter of s3.putObject can take a Buffer, Typed Array, Blob, String, or ReadableStream. The best one of those to use in most cases would be a ReadableString. You can create a ReadableString from any file using the createReadStream() function in the fs module.
So, that part your code would look something like:
finder.on('file', function (file, stat) {
console.log(file);
var params = {
Bucket: BUCKET_NAME,
Key: file,
Body: fs.createReadStream(file) // NOTE: You might need to adjust "file" so that it's either an absolute path, or relative to your code's directory.
};
s3.putObject(params, function(err) {
if(err) {
console.log(err);
}
else {
console.log("Success!");
}
});
});
I also want to point out that you might run in to a problem with this code if you pass it a directory with a lot of files. putObject is an asynchronous function, which means it'll be called and then the code will move on to something else while it's doing its thing (ok, that's a gross simplification, but you can think of it that way). What that means in terms of this code is that you'll essentially be uploading all the files it finds at the same time; that's not good.
What I'd suggest is to use something like the async module to queue your file uploads so that only a few of them happen at a time.
Essentially you'd move the code you have in your file event handler to the queue's worker method, like so:
var async = require('async');
var uploadQueue = async.queue(function(file, callback) {
var params = {
Bucket: BUCKET_NAME,
Key: file,
Body: fs.createReadStream(file) // NOTE: You might need to adjust "file" so that it's either an absolute path, or relative to your code's directory.
};
s3.putObject(params, function(err) {
if(err) {
console.log(err);
}
else {
console.log("Success!");
}
callback(err); // <-- Don't forget the callback call here so that the queue knows this item is done
});
}, 2); // <-- This "2" is the maximum number of files to upload at once
Note the 2 at the end there, that specifies your concurrency which, in this case, is how many files to upload at once.
Then, your file event handler simply becomes:
finder.on('file', function (file, stat) {
uploadQueue.push(file);
});
That will queue up all the files it finds and upload them 2 at a time until it goes through all of them.
An easier and arguably more efficient solution may be to just tar up the directory and upload that single tar file (also gzipped if you want). There are tar modules on npm, but you could also just spawn a child process for it too.

Generate Markdown from template

I'd like to use the node-pandoc module to generate PDFs from Markdown. But I need to create those Markdowns on fly. Are there any templating engines for node.js that can generate plaintext/markdown?
I've recently used underscore's template with plain text files written in rho (which is also a plain-text-to-html tool, like Markdown) to generate plain text documents with dynamic data:
Here's the code of my module (omit the caching if you don't need it):
// compiler.js
'use strict';
var fs = require('fs')
, path = require('path')
, _ = require('underscore');
var cache = {};
exports.getTemplate = function(templateId, cb) {
// Use your extension here
var file = path.join(__dirname, templateId + ".rho");
fs.stat(file, function(err, stat) {
if (err) return cb(err);
// Try to get it from cache
var cached = cache[templateId];
if (cached && cached.mtime >= stat.mtime)
return cb(null, cached.template);
// Read it from file
fs.readFile(file, { encoding: 'utf-8' }, function(err, data) {
if (err) return cb(err);
// Compile it
var template = _.template(data);
// Cache it
cache[templateId] = {
mtime: stat.mtime,
template: template
};
// Return it
return cb(null, template);
});
});
};
exports.compile = function(templateId, data, cb) {
exports.getTemplate(templateId, function(err, template) {
if (err) return cb(err);
try {
return cb(null, template(data));
} catch (e) {
return cb(e);
}
});
}
Now the usage. Assume you have hello.rho with following contents:
# Hello, <%= name %>!
We are happy to have you here, <%= name %>!
You can compile it like this:
require('./compiler').compile('hello', { name: 'World' }, function(err, text) {
if (err) // Handle the error somehow
return console.log(err);
console.log(text);
// You'll get "# Hello, World!\n\nWe're happy to have you here, World!"
// Now chain the compilation to rho, markdown, pandoc or whatever else.
});
If you don't like underscore, then I guess ejs would also do the job just fine.
Take a look at grunt-readme, which - although focused on generating README documentation from templates - is a good example of how you can generate markdown docs from templates.

Piping multiple file streams using Node.js

I want to stream multiple files, one after each other, to the browser. To illustrate, think of having multiple CSS files which shall be delivered concatenated as one.
The code I am using is:
var directory = path.join(__dirname, 'css');
fs.readdir(directory, function (err, files) {
async.eachSeries(files, function (file, callback) {
if (!endsWith(file, '.css')) { return callback(); } // (1)
var currentFile = path.join(directory, file);
fs.stat(currentFile, function (err, stats) {
if (stats.isDirectory()) { return callback(); } // (2)
var stream = fs.createReadStream(currentFile).on('end', function () {
callback(); // (3)
});
stream.pipe(res, { end: false }); // (4)
});
}, function () {
res.end(); // (5)
});
});
The idea is that I
filter out all files that do not have the file extension .css.
filter out all directories.
proceed with the next file once a file has been read completely.
pipe each file to the response stream without closing it.
end the response stream once all files have been piped.
The problem is that only the first .css file gets piped, and all remaining files are missing. It's as if (3) would directly jump to (5) after the first (4).
The interesting thing is that if I replace line (4) with
stream.on('data', function (data) {
console.log(data.toString('utf8'));
});
everything works as expected: I see multiple files. If I then change this code to
stream.on('data', function (data) {
res.write(data.toString('utf8'));
});
all files expect the first are missing again.
What am I doing wrong?
PS: The error happens using Node.js 0.8.7 as well as using 0.8.22.
UPDATE
Okay, it works if you change the code as follows:
var directory = path.join(__dirname, 'css');
fs.readdir(directory, function (err, files) {
var concatenated = '';
async.eachSeries(files, function (file, callback) {
if (!endsWith(file, '.css')) { return callback(); }
var currentFile = path.join(directory, file);
fs.stat(currentFile, function (err, stats) {
if (stats.isDirectory()) { return callback(); }
var stream = fs.createReadStream(currentFile).on('end', function () {
callback();
}).on('data', function (data) { concatenated += data.toString('utf8'); });
});
}, function () {
res.write(concatenated);
res.end();
});
});
But: Why? Why can't I call res.write multiple times instead of first summing up all the chunks, and then write them all at once?
Consider also using multistream, that allows you to combine and emit multiple streams one after another.
The code was perfectly fine, it was the unit test that was wrong ...
Fixed that, and now it works like a charme :-)
May help someone else:
const fs = require("fs");
const pth = require("path");
let readerStream1 = fs.createReadStream(pth.join(__dirname, "a.txt"));
let readerStream2 = fs.createReadStream(pth.join(__dirname, "b.txt"));
let writerStream = fs.createWriteStream(pth.join(__dirname, "c.txt"));
//only readable streams have "pipe" method
readerStream1.pipe(writerStream);
readerStream2.pipe(writerStream);
I also checked Rocco's answer and its working like a charm:
//npm i --save multistream
const multi = require('multistream');
const fs = require('fs');
const pth = require("path");
let streams = [
fs.createReadStream(pth.join(__dirname, "a.txt")),
fs.createReadStream(pth.join(__dirname, "b.txt"))
];
let writerStream = fs.createWriteStream(pth.join(__dirname, "c.txt"));
//new multi(streams).pipe(process.stdout);
new multi(streams).pipe(writerStream);
and to send the results to client:
const multi = require('multistream');
const fs = require('fs');
const pth = require("path");
const exp = require("express");
const app = exp();
app.listen(3000);
app.get("/stream", (q, r) => {
new multi([
fs.createReadStream(pth.join(__dirname, "a.txt")),
fs.createReadStream(pth.join(__dirname, "b.txt"))
]).pipe(r);
});

Resources