This is NOT a dupe of this question. This question is NOT about Windows. It's a general question across OSes.
Is there an efficient way to get the correct case of a filename in node.js other than getting the directory and finding the matching name?
Example: Assume I have a folder with 3 files
+-someFolder
+-fooBar.txt
+-Moo.txt
+-ReadMe.txt
I want a function that passed somefolder/readme.txt
returns someFolder/ReadMe.txt
.
AFAICT the only way to do that is to call fs.readDir
or fs.readDirSync
and see if there is a matching file, something like
const fs = require('fs');
const path = require('path');
function getActualFilename(filename) {
if(!fs.existsSync(filename)) {
throw new Error(`${filename} does not exist`);
}
return getActualFilenameImpl(filename);
}
function getActualFilenameImpl(filename) {
const lcFilename = path.basename(filename).toLowerCase();
// handles passing in `c:\\`
if (!lcFilename) {
return filename.toUpperCase();
}
const dirname = path.dirname(filename);
let filenames;
try {
filenames = fs.readdirSync(dirname);
} catch (e) {
// we already verified the path exists above so if this
// happens it means the OS won't let use get a listing (UNC root on windows)
// so it's the best we can do
return filename;
}
const matches = filenames.filter(name => lcFilename === name.toLowerCase());
if (!matches.length) {
throw new Error(`${filename} does not exist`);
}
const realname = matches[0];
if (dirname !== '.') {
if (dirname.endsWith('/') || dirname.endsWith('\\')) {
return path.join(dirname, realname);
} else {
return path.join(getActualFilenameImpl(dirname), realname);
}
} else {
return realname;
}
}
The code above is pretty hacky. Trying in on different things has made it clear there's lots of edge cases. On Windows in particular UNC paths fail since you can't call fs.readdirSync
once you get to the network path root. I have no idea what functions to call to figure out where that path separates and then how to get the correct case path for that which is probably an entirely separate set of Windows API calls (like calling whatever functions net use
uses to show shares) etc...
I did notice path.dirname
stops removing the trailing slash when it gets to a UNC path so using that to try to figure out when stop trying.
Notes:
I get that for example on Linux (and optionally on Mac) the file system may be case sensitive and I'd have to check for that but I'm mostly concerned with Windows and standard macOS and will deal with case-sensitive issues later.
I also get that JavaScript's toLowerCase might not match the OSes concept of case insensitivity so if there is a solution that takes that into account that would also be great!
I get that I could cache results or directory listing for a speed up but was wondering if there is some other function to use that doesn't read the entire directory listing.
I'm actually trying to solve several problems and am open to other suggestions
Problem 1: What filename to store in an app specific database. It seems best to store the actual filename. See #3
Problem 2: Figuring out if 2 filenames reference the same file/folder. So if the user specifies SomeFolder/foobar.txt
and somefolder/FOOBAR.txt
I don't want that to appear as 2 separate files if they are actually the same file. I need my app to know they reference the same file. I think I can call fs.stat
for this and check if the ino
field matches?
Problem 3: Related to problem 1, reloading metadata related to the file. If the user specifies SomeFolder/foobar.txt
at some point and my app generates metadata related to the file, then at some other point in time they specify somefolder/FOOBAR.txt
I need to find the matching metadata. My current thinking is by looking up the actual filename and using that to match with this problem would be solved. Although I suppose if they rename the file from FooBar.txt
to foobar.txt
it would lose the metadata. Not sure I care about that situation though since if they rename from FooBar.txt
to SomethingElse.txt
I definitely do not care if I lose the metadata.
That said, maybe I should store the ino
as the key in my DB? Not sure I'm comfortable with that idea yet but it's a possibility and would love to know if others do that. Some checking reveals that at least on macOS the ino stays the same across moves and rename on the same drive which would be a good thing for my use case. On the other hand I'd assume ino is only valid per file system so if I have 2 different drives mounted I could get clashing inos. I could use dev
and ino
as a key as in
const stat = fs.statSync(filename);
const key = `${stat.dev}:${stat.ino}`;
Though I have no idea if stat.dev
is always the same with removable storage. I assume it's not. So it seems like filename as key is probably better?