Xplornet, my ISP, offers 50 MB of free hosting. More interestingly, it’s a public hostname that I don’t have to worry about setting up or maintaining! While mucking around with JavaScript front end frameworks, I figured that’d be a neat place to drop my work. Right off the bat I was a little bit wary of the offering, as 50 MB is a weirdly tiny amount of space, which likely means Xplornet has no idea what they’re doing when it comes to web hosting. Upon signing up and receiving my concerningly short password in plaintext, which I also don’t seem to be able to change, I figured I was bang on with my suspicions. Nothing private going on that server, that’s for sure. Regardless, I figured I’d give it a try and see what I could do with it.
It looks like the only way to get files to the server is via FTP (of course no SFTP). Xplornet gave me an account with access to a less than ideal front end for loading up files, but that is all GUI driven and generally painful. I want to deploy my front end automatically, so time to figure out a little FTP.
FTP in JavaScript
I’m presently trying to develop my JavaScript skills, so I figured I’d start writing up a deployment script in JS. So far as I can tell, the FTP ecosystem is pretty sparse in the JavaScript world. I landed on jsftp as the library for backing the interaction, but it’s a bit rough around the edges. I figured I’d keep the script simple: delete everything in /public
and replace it with the output of my build. I immediately tripped over FTP… I don’t really know anything about it, beyond using clients to move files around every now and then, but it appears as though deleting a non-empty file is difficult or maybe impossible?
For lack of rm -rf
When I can’t delete non-empty directories, the only other solution I can think of is to walk the directory and delete everything from the bottom up. So that’s what I decided to do! Note that this is literally the first iteration that managed to delete all the files in one shot, so hold off on the judgement… If I have spare time I might bundle it up into a NPM module, we’ll see how it goes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const JSFtp = require("jsftp"); | |
const path = require('path'); | |
var ftp = new JSFtp({ | |
host: process.env.HOST, | |
port: process.env.PORT, | |
user: process.env.USERNAME, | |
pass: process.env.PASSWORD | |
}); | |
ftp.auth( | |
ftp.user, | |
ftp.pass, | |
function (err, data) { | |
if (err) { | |
console.log("Error is: ", err); | |
return; | |
} | |
removeAllFilesInPublicFolder(ftp); | |
} | |
); | |
function removeAllFilesInPublicFolder(ftp) { | |
walk(ftp, '/public') | |
.then((results) => { | |
let files = flatten(results) | |
.filter(Boolean); | |
console.log("About to try deleting ", files.map((file) => file.filepath)); | |
// Non-empty directories can't be removed, should I remove all files first? | |
let deletions = files.map((file) => { | |
if (isDirectory(file)) { | |
return deleteDirectory(ftp, file); | |
} else { | |
return deleteFile(ftp, file); | |
} | |
}); | |
return Promise.all(deletions); | |
}).then((deletionResults) => { | |
console.log("Deletion results: ", deletionResults); | |
ftp.destroy(); | |
}).catch((error) => { | |
console.log("Error: ", error); | |
ftp.destroy(); | |
}) | |
} | |
function walk(ftp, directory) { | |
return list(ftp, directory) | |
.then((files) => { | |
if (files.length === 0) { | |
return Promise.resolve(); | |
} | |
return Promise.all(files.map((file) => { | |
file.filepath = path.join(directory, file.name); | |
let promises = []; | |
if (isDirectory(file)) { | |
promises.push(walk(ftp, path.join(directory, file.name))); | |
} | |
// Make sure the directory is after the files in the list of promises | |
promises.push(Promise.resolve(file)); | |
return Promise.all(promises); | |
})); | |
}); | |
} | |
function deleteDirectory(ftp, directory) { | |
return new Promise((resolve, reject) => { | |
ftp.raw('rmd', directory.filepath, (error, result) => { | |
if (error) { | |
return reject(error); | |
} else { | |
return resolve(result); | |
} | |
}); | |
}) | |
} | |
function deleteFile(ftp, file) { | |
return new Promise((resolve, reject) => { | |
ftp.raw('dele', file.filepath, (error, result) => { | |
if (error) { | |
return reject(error); | |
} else { | |
return resolve(result); | |
} | |
}); | |
}) | |
} | |
function list(ftp, directory) { | |
return new Promise((resolve, reject) => { | |
ftp.ls(directory, (error, files) => { | |
if (error) { | |
reject(error); | |
return; | |
} | |
resolve(files); | |
}); | |
}); | |
} | |
function isDirectory(file) { | |
return file.type === 1; | |
} | |
const flatten = list => list.reduce( | |
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] | |
); |
And that managed to do it. The next piece will be dumping the build output on the server, and then I should be good to go to actually use those 50 MB…