Can I use require("path").join to safely concatenate urls? - node.js

Is this safe to use require("path").join to concatenate URLs, for example:
require("path").join("http://example.com", "ok");
//returns 'http://example.com/ok'
require("path").join("http://example.com/", "ok");
//returns 'http://example.com/ok'
If not, what way would you suggest for doing this without writing code full of ifs?

No. path.join() will return incorrect values when used with URLs.
It sounds like you want new URL(). From the WHATWG URL Standard:
new URL('/one', 'http://example.com/').href // 'http://example.com/one'
new URL('/two', 'http://example.com/one').href // 'http://example.com/two'
Note that url.resolve is now marked as deprecated in the Node docs.
As Andreas correctly points out in a comment, url.resolve (also deprecated) would only help if the problem is as simple as the example. url.parse also applies to this question because it returns consistently and predictably formatted fields via the URL object that reduces the need for "code full of ifs". However, new URL() is also the replacement for url.parse.

No, you should not use path.join() to join URL elements.
There's a package for doing that now. So rather than reinvent the wheel, write all your own tests, find bugs, fix them, write more tests, find an edge case where it doesn't work, etc., you could use this package.
url-join
https://github.com/jfromaniello/url-join
Install
npm install url-join
Usage
var urljoin = require('url-join');
var fullUrl = urljoin('http://www.google.com', 'a', '/b/cd', '?foo=123');
console.log(fullUrl);
Prints:
'http://www.google.com/a/b/cd?foo=123'

This can be accomplished by a combination of Node's path and URL:
Require the packages:
const nodeUrl = require('url')
const nodePath = require('path')
Start by making a URL object to work with:
> const myUrl = new nodeUrl.URL('https://example.com')
Use pathname= and path.join to construct any possible combination:
> myUrl.pathname = nodePath.join('/search', 'for', '/something/')
'/search/for/something/'
(you can see how liberal path.join is with arguments)
At this point your URL reflects the ultimate desired result:
> myUrl.toString()
'https://example.com/search/for/something/'
Why this approach?
This technique uses built-in libraries. The less third-party dependencies the better, when it comes to CVEs, maintenance, etc.
Nothing will be more proven or better tested than standard libs.
PS: Never manipulate URLs as strings!
When I review code I'm adamant about never manipulating URLs as strings manually. For one, look how complicated the spec is.
Secondly, the absence/presence of a trailing/prefixed slash (/) should not cause everything to break! You should never do:
const url = `${baseUrl}/${somePath}`
and especially not:
uri: host + '/' + SAT_SERVICE + '/' + CONSTELLATION + '/',
Of which I have seen.

Axios has a helper function that can combine URLs.
function combineURLs(baseURL, relativeURL) {
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL;
}
Source:
https://github.com/axios/axios/blob/fe7d09bb08fa1c0e414956b7fc760c80459b0a43/lib/helpers/combineURLs.js

The WHATWG URL object constructor has a (input, base) version, and the input can be relative using /, ./, ../. Combine this with path.posix.join and you can do anything:
const {posix} = require ("path");
const withSlash = new URL("https://example.com:8443/something/");
new URL(posix.join("a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/something/a/b/c'
new URL(posix.join("./a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/something/a/b/c'
new URL(posix.join("/a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/a/b/c'
new URL(posix.join("../a", "b", "c"), withSlash).toString(); // 'https://example.com:8443/a/b/c'
const noSlash = new URL("https://example.com:8443/something");
new URL(posix.join("./a", "b", "c"), noSlash).toString(); // 'https://example.com:8443/a/b/c'

No! On Windows path.join will join with backslashes. HTTP urls are always forward slashes.
How about
> ["posts", "2013"].join("/")
'posts/2013'

When I tried PATH for concatenating url parts I run into problems.
PATH.join stripes '//' down to '/' and this way invalidates an absolute url (eg. http://... -> http:/...).
For me a quick fix was:
baseurl.replace(/\/$/,"") + '/' + path.replace(/^\//,"") )
or with the solution posted by Colonel Panic:
[pathA.replace(/^\/|\/$/g,""),pathB.replace(/^\/|\/$/g,"")].join("/")

We do it like this:
var _ = require('lodash');
function urlJoin(a, b) {
return _.trimEnd(a, '/') + '/' + _.trimStart(b, '/');
}

If you're using lodash, you can use this simple oneliner:
// returns part1/part2/part3
['part1/', '/part2', '/part3/'].map((s) => _.trim(s, '/')).join('/')
inspired by #Peter Dotchev's answer

If you use Angular, you can use Location:
import { Location } from '#angular/common';
// ...
Location.joinWithSlash('beginning', 'end');
Works only on 2 arguments though, so you have to chain calls or write a helper function to do that if needed.

This is what I use:
function joinUrlElements() {
var re1 = new RegExp('^\\/|\\/$','g'),
elts = Array.prototype.slice.call(arguments);
return elts.map(function(element){return element.replace(re1,""); }).join('/');
}
example:
url = joinUrlElements(config.mgmtServer, '/v1/o/', config.org, '/apps');

There are other working answers, but I went with the following. A little path.join/URL combo.
const path = require('path');
//
const baseUrl = 'http://ejemplo.mx';
// making odd shaped path pieces to see how they're handled.
const pieces = ['way//', '//over/', 'there/'];
//
console.log(new URL(path.join(...pieces), baseUrl).href);
// http://ejemplo.mx/way/over/there/
// path.join expects strings. Just an example how to ensure your pieces are Strings.
const allString = ['down', 'yonder', 20000].map(String);
console.log(new URL(path.join(...allString), baseUrl).href);
// http://ejemplo.mx/down/yonder/20000

By the time posting this answer url.resolve() is deprecated;
I did following to join to path in Nodejs:
const path = require('path');
const url = require('url');
let myUrl = new URL('http://ignore.com');
myUrl.pathname=path.join(firstpath, secondpath);
console.log(myUrl.pathname)
This approach logs correct url path and it works for my case.
What is your opinion about this approach?
Thanks

Typescript custom solution:
export function pathJoin(parts: string[], sep: string) {
return parts
.map(part => {
const part2 = part.endsWith(sep) ? part.substring(0, part.length - 1) : part;
return part2.startsWith(sep) ? part2.substr(1) : part2;
})
.join(sep);
}
expect(pathJoin(['a', 'b', 'c', 'd'], '/')).toEqual('a/b/c/d');
expect(pathJoin(['a/', '/b/', 'c/', 'd'], '/')).toEqual('a/b/c/d');
expect(pathJoin(['http://abc.de', 'users/login'], '/')).toEqual('http://abc.de/users/login');

My solution
path.join(SERVER_URL, imageAbsolutePath).replace(':/','://');
Edit: if you want to support windows enviroments
path.join(SERVER_URL, imageAbsolutePath).replace(/\\/g,'/').replace(':/','://');
The second solution will replace all the backslashes, so url parts like querystring and hash may be altered too, but the topic is joining just the url path, so I don't consider it an issue.

The combination of the built-in path and URL libraries provides the best solution.
The answers above, however, do not handle the case where you have a relative url (ie: "../foo") that you want to add to an existing URL (ie: "http://example.com/test/bar"). Simply doing new URL("../foo","http://example.com/test/bar").href would yield "http://example.com/foo", erroneously (for me) discarding the remainder of the original path.
A simple solution is:
var base = "http://example.com:8080/foo/bar";
var rel = "../test";
var resolved = new URL( path.resolve(new URL(base).pathname, rel ), base ).href;
// Result: http://example.com:8080/foo/test
Note: If you care about URL Search Params (which seems unlikely in this scenario), you would save each of the pieces above to discrete variables and set the new url.searchParams = oldUrl.searchParams before getting the href output.

Related

Node.js fs.writeFile() not creating new files?

I need to create many .json files for the system i am trying to develop. To do this, i ran a for loop over the file names i needed, then used fs.writeFileSync('filename.json', [data]).
However, when trying to open these later, and when I try to find them in the project folder, I cannot find them anywhere.
I have tried writing in a name that was less complex and should have appeared in the same directory as my script, however that was fruitless as well. To my understanding, even if my file name wasn't what I expected it to be, I should get at least something, somewhere, however I end up with nothing changed.
My current code looks like this:
function addEmptyDeparture(date) {
fs.readFileSync(
__dirname + '/reservations/templates/wkend_dep_template.json',
(err, data) => {
if (err) throw err
fs.writeFileSync(
getDepartureFileName(date),
data
)
}
)
}
function getDepartureFileName(date){
return __dirname + '/reservations/' +
date.getFullYear() +
'/departing/' +
(date.getMonth() + 1).toString().padStart(2, "0") +
date.getDate().toString().padStart(2, "0") +
'.json'
}
Where data is the JSON object returned from fs.readFileSync() and is immediately written into fs.writeFileSync(). I don't think I need to stringify this, since it's already a JSON object, but I may be wrong.
The only reason I think it's not working at all (as opposed to simply not showing up in my project) is that, in a later part of the code, we have this:
fs.readFileSync(
getDepartureFileName(date)
)
.toString()
which is where I get an error for not having a file by that name.
It is also worth noting that date is a valid date object, as I was able to test that part in a fiddle.
Is there something I'm misunderstanding in the effects of fs.writeFile(), or is this not the best way to write .json files for use on a server?
You probably are forgetting to stringify the data:
fs.writeFileSync('x.json', JSON.stringify({id: 1}))
I have tried to create similar case using a demo with writeFileSync() creating different files and adding json data to these ,using a for loop. In my case it works . Each time a new file name is created . Here is my GitHub for the same :-
var fs = require('fs');
// Use readFileSync() method
// Store the result (return value) of this
// method in a variable named readMe
for(let i=0 ; i < 4 ; i++) {
var readMe = JSON.stringify({"data" : i});
fs.writeFileSync('writeMe' + i + '.json', readMe, "utf8");
}
// Store the content and read from
// readMe.txt to a file WriteMe.txt
Let me know if this what you have been trying at your end.

Nodejs absolute paths in windows with forward slash

Can I have absolute paths with forward slashes in windows in nodejs? I am using something like this :
global.__base = __dirname + '/';
var Article = require(__base + 'app/models/article');
But on windows the build is failing as it is requiring something like C:\Something\Something/apps/models/article. I aam using webpack. So how to circumvent this issue so that the requiring remains the same i.e. __base + 'app/models/src'?
I know it is a bit late to answer but I think my answer will help some visitors.
In Node.js you can easily get your current running file name and its directory by just using __filename and __dirname variables respectively.
In order to correct the forward and back slash accordingly to your system you can use path module of Node.js
var path = require('path');
Like here is a messed path and I want it to be correct if I want to use it on my server. Here the path module do everything for you
var randomPath = "desktop//my folder/\myfile.txt";
var correctedPath = path.normalize(randomPath); //that's that
console.log(correctedPath);
desktop/my folder/myfile.txt
If you want the absolute path of a file then you can also use resolve function of path module
var somePath = "./img.jpg";
var resolvedPath = path.resolve(somePath);
console.log(resolvedPath);
/Users/vikasbansal/Desktop/temp/img.jpg
it's 2020, 5 years from the question was published, but I hope that for somebody my answer will be useful. I've used the replace method, here is my code(express js project):
const viewPath = (path.join(__dirname, '../views/')).replace(/\\/g, '/')
exports.articlesList = function(req, res) {
res.sendFile(viewPath + 'articlesList.html');
}
I finally did it like this:
var slash = require('slash');
var dirname = __dirname;
if (process.platform === 'win32') dirname = slash(dirname);
global.__base = dirname + '/';
And then to require var Article = require(__base + 'app/models/article');. This uses the npm package slash (which replaces backslashes by slashes in paths and handles some more cases)
The accepted answer doesn't actually answer the question most people come here for.
If you're looking to normalize all path separators (possibly for string work), here's what you need.
All the code segments have the node.js built-in module path imported to the path variable.
They also have the variable they work from stored in the immutable variable str, unless otherwise specified.
If you have a string, here's a quick one-liner normalize the string to a forward slash (/):
const answer = path.resolve(str).split(path.sep).join("/");
You can normalize to any other separator by replacing the forward slash (/).
If you want just an array of the parts of the path, use this:
const answer = path.resolve(str).split(path.sep);
Once you're done with your string work, use this to create a path able to be used:
const answer = path.resolve(str);
From an array, use this:
// assume the array is stored in constant variable arr
const answer = path.join(...arr);
I recommend against this, as it is patching node itself, but... well, no changes in how you require things.
(function() {
"use strict";
var path = require('path');
var oldRequire = require;
require = function(module) {
var fixedModule = path.join.apply(path, module.split(/\/|\\/));
oldRequire(fixedModule);
}
})();
This is the approach I use, to save some processing:
const path = require('path');
// normalize based on the OS
const normalizePath = (value: string): string {
return path.sep === '\'
? value.replace(/\\/g, '/')
: value;
}
console.log('abc/def'); // leaves as is
console.log('abc\def'); // on windows converts to `abc/def`, otherwise leave as is
Windows uses \, Linux and mac use / for path prefixes
For Windows : 'C:\\Results\\user1\\file_23_15_30.xlsx'
For Mac/Linux: /Users/user1/file_23_15_30.xlsx
If the file has \ - it is windows, use fileSeparator as \, else use /
let path=__dirname; // or filePath
fileSeparator=path.includes('\')?"\":"/"
newFilePath = __dirname + fileSeparator + "fileName.csv";
Use path module
const path = require("path");
var str = "test\test1 (1).txt";
console.log(str.split(path.sep)) // This is only on Windows

Denormalizing path separator in Node.js under Windows

In Windows, path module of Node.js produces all paths with backslash. It seems like it calls its own normalize() function when other functions (like resolve()) are called.
Is there a way to de-normalize the paths (with forward slash), without explicit replacement all over the place?
I tried setting path.sep = '/', but resolve() still produces paths with \\.
You can path.sep or path.parse to disassemble path and then join it as you like.
I had that problem with Windows 10. I resolved it with this script:
var path = require('path');
path.join2 = path.join;
path.sep = '/';
path.join = function(){
var res = path.join2.apply({}, arguments);
res = res.replace(/\\/g, path.sep);
return res;
}
//rest of your code

Get file name from absolute path in Nodejs?

How can I get the file name from an absolute path in Nodejs?
e.g. "foo.txt" from "/var/www/foo.txt"
I know it works with a string operation, like fullpath.replace(/.+\//, ''),
but I want to know is there an explicit way, like file.getName() in Java?
Use the basename method of the path module:
path.basename('/foo/bar/baz/asdf/quux.html')
// returns
'quux.html'
Here is the documentation the above example is taken from.
To get the file name portion of the file name, the basename method is used:
var path = require("path");
var fileName = "C:\\Python27\\ArcGIS10.2\\python.exe";
var file = path.basename(fileName);
console.log(file); // 'python.exe'
If you want the file name without the extension, you can pass the extension variable (containing the extension name) to the basename method telling Node to return only the name without the extension:
var path = require("path");
var fileName = "C:\\Python27\\ArcGIS10.2\\python.exe";
var extension = path.extname(fileName);
var file = path.basename(fileName,extension);
console.log(file); // 'python'
var path = require("path");
var filepath = "C:\\Python27\\ArcGIS10.2\\python.exe";
var name = path.parse(filepath).name;
console.log(name); //python
var base = path.parse(filepath).base;
console.log(base); //python.exe
var ext = path.parse(filepath).ext;
console.log(ext); //.exe
For those interested in removing extension from filename, you can use
https://nodejs.org/api/path.html#path_path_basename_path_ext
path.basename('/foo/bar/baz/asdf/quux.html', '.html');
If you already know that the path separator is / (i.e. you are writing for a specific platform/environment), as implied by the example in your question, you could keep it simple and split the string by separator:
'/foo/bar/baz/asdf/quux.html'.split('/').pop()
That would be faster (and cleaner imo) than replacing by regular expression.
Again: Only do this if you're writing for a specific environment, otherwise use the path module, as paths are surprisingly complex. Windows, for instance, supports / in many cases but not for e.g. the \\?\? style prefixes used for shared network folders and the like. On Windows the above method is doomed to fail, sooner or later.
path is a nodeJS module meaning you don't have to install any package for using its properties.
import path from 'path'
const dir_name = path.basename('/Users/Project_naptha/demo_path.js')
console.log(dir_name)
// returns
demo_path.js
In NodeJS, __filename.split(/\|//).pop() returns just the file name from the absolute file path on any OS platform.
Why need to care about remembering/importing an API while this regex approach also letting us recollect our regex skills.
So Nodejs comes with the default global variable called '__fileName' that holds the current file being executed
My advice is to pass the __fileName to a service from any file , so that the retrieval of the fileName is made dynamic
Below, I make use of the fileName string and then split it based on the path.sep. Note path.sep avoids issues with posix file seperators and windows file seperators (issues with '/' and '\'). It is much cleaner. Getting the substring and getting only the last seperated name and subtracting it with the actulal length by 3 speaks for itself.
You can write a service like this (Note this is in typescript , but you can very well write it in js )
export class AppLoggingConstants {
constructor(){
}
// Here make sure the fileName param is actually '__fileName'
getDefaultMedata(fileName: string, methodName: string) {
const appName = APP_NAME;
const actualFileName = fileName.substring(fileName.lastIndexOf(path.sep)+1, fileName.length - 3);
//const actualFileName = fileName;
return appName+ ' -- '+actualFileName;
}
}
export const AppLoggingConstantsInstance = new AppLoggingConstants();

NODE.JS - how to handle a mix of OS and URL-style "paths" correctly?

In my node.js app I have functions which can be passed either
OS-style paths e.g. c:\my\docs\mydoc.doc (or /usr/docs/mydoc.doc or whatever is local)
File URLS e.g. file://c:/my/docs/mydoc.doc (which I'm not sure about the validity of '\'s in??)
Either way, I need to check to see if they refer to a specific location which will always exist as a local OS-style path e.g. c:\mydata\directory\ or /usr/mydata/directory
Obviously for OS-style paths I can just compare them as strings - they SHOULD always be the same (they're created with path) but FILE:// URLS don't necessarily use path.sep and so won't "string match"?
Any suggestions as to the best way to handle this (I'm personally tempted to break EVERYTHING by one-or-more slashes of either sort and then check each piece??
I'm going to post my own take on this - as it came from a suggestion I got from someone on Facebook (no - really!) and it uses path in a way it probably wasn't intended for - e.g. I'm not sure it's the right 'solution' - Im not sure I'm not exploiting path a bit.
The Facebook tip was that path is really just a utility for handing strings with "/" and "\" separators - it ignores everything else - doesn't care what's in there at all.
On that basis, we can use
path.normalize(ourpath)
which will convert all separators to the local OS preferred ones (path.sep)
That means they will match my OS-style directory (which is also made with path) and so I can compare those - without resorting to manually gutting-out slashes...
e.g.
Before
file://awkward/use/of\\slashes\in/this/path
After
file:\awkward\use\of\slashes\in\this\path (Windows)
or
file:/awkward/use/of/slashes/in/this/path (everywhere else)
Removing file:// before (or file: + path.sep after) = local OS-style path!?
Just do some manipulation of the string and check to make sure they are the same after correcting for the differences:
var path = require('path');
var pathname = "\\usr\\home\\newbeb01\\Desktop\\testinput.txt";
var pathname2 = "file://usr/home/newbeb01/Desktop/testinput.txt"
if(PreparePathNameForComparing(pathname) == PreparePathNameForComparing(pathname2))
{ console.log("Same path"); }
else
{ console.log("Not the same path"); }
function PreparePathNameForComparing(pathname)
{
var returnString = pathname;
//try to find the file uri prefix, if there strip it off
if(pathname.search("file://") != -1 || pathname.search("FILE://") != -1)
{ returnString = pathname.substring(6, pathname.length); }
//now make all slashes the same
if(path.sep === '\\') //replace all '/' with '\\'
{ returnString = returnString.replace(/\//g, '\\'); }
else //replace all '\\' with '/'
{ returnString = returnString.replace(/\\/g, '/'); }
return returnString;
}
I checked to see if the URI path name indicator "file://" was there, if so, I deleted it off my compare string. Then I normalized based on the path separator node path module will give me. This way it should work in either Linux or Windows environment.

Resources