I've dealt with a different situation before and handled it with sharding. It's pretty easy to retrofit in code, and it's not hard to do a one-time move of existing files to the new layout.
Simply put, you make an algorithm like:
let hash = md5(filename)
let folderPath = path.join(root, hash.substr(0,1), hash.substr(0,2), hash.substr(0,3), filename)
The original name can be used to derive the sharded path, so you don't need a lookup database or anything crazy like that.You end up storing files like:
.../7/7f/7fa/somefile.png
This scales to whatever depth you want: with 3 levels and 1.2 million files, you can expect about 1200000/16^3 = 293 files per directory, which is trivial.There's lots of other strategies, too, depending on your needs:
# filename are already GUIDs or hashes:
.../1/1f/1fd/1fd2dd27-d307-47b2-b37d-11903fd0f03d.png
# date-based to make cleaning up old files easy:
.../2023/01/19/app.log
# hash by existing numeric identifier (like customerid) using modulus:
.../customers/4/94/52194/customerfile.dat