diff --git a/.gitignore b/.gitignore index 57a9aee..4f87cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ +temp/ + package-lock.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1e73159 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +.github/ + +temp/ + +.eslintrc +.travis.yml +appveyor.yml + +logo.png diff --git a/README.md b/README.md index 83395e0..8d70c86 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,36 @@ $ echo $UNITY_URL ## API -### `getUnityUrls(string filter)` +### `checkCacheExpiry(string path, integer ttl)` + +```javascript +const { checkCacheExpiry } = require("get-unity"); + +checkCacheExpiry("./data/editor-installers.json", 3600000) + .then(() => console.log("Cache is ok.")) + .catch(() => console.log("Cache has expired.")); +``` + +### `fetchWithLocalCache(string path, integer ttl)` + +```javascript +const { fetchWithLocalCache } = require("get-unity"); + +fetchWithLocalCache( + "https://unity3d.com/get-unity/download/archive", + "./temp/archive.html", + 3600000 +).then(response => console.log(response)); +``` + +### `getUnityUrls(string filter [, string filePath])` ```javascript const { getUnityUrls } = require("get-unity"); -getUnityUrls("2019").then(urls => console.log(urls)); +getUnityUrls("2019", "./data/editor-installers.json").then(urls => + console.log(urls) +); ``` Output: @@ -60,7 +84,7 @@ Output: ```json { "mac": "https://download.unity3d.com/download_unity/5f859a4cfee5/MacEditorInstaller/Unity-2019.2.11f1.pkg", - "win64": "https://netstorage.unity3d.com/unity/5f859a4cfee5/Windows64EditorInstaller/UnitySetup64-2019.2.11f1.exe" + "win64": "https://download.unity3d.com/download_unity/5f859a4cfee5/Windows64EditorInstaller/UnitySetup64-2019.2.11f1.exe" } ``` @@ -80,3 +104,13 @@ Output: ``` 2019.2.9f1 ``` + +### `updateEditorInstallers([string filePath])` + +```javascript +const { updateEditorInstallers } = require("get-unity"); + +updateEditorInstallers("./data/editor-installers.json").then(() => + console.log("Done") +); +``` diff --git a/bin/index.js b/bin/index.js index 26b32e5..88dda68 100755 --- a/bin/index.js +++ b/bin/index.js @@ -2,6 +2,7 @@ const os = require('os'); const {readFileSync} = require('fs'); +const {join} = require('path'); const chalk = require('chalk'); const meow = require('meow'); @@ -10,6 +11,7 @@ const updateNotifier = require('update-notifier'); const pkg = require('../package.json'); +const updateEditorInstallers = require('../lib/update-editor-installers'); const getUnityUrls = require('../lib/get-unity-urls'); const {parseVersionFromString} = require('../lib/parsers'); @@ -19,9 +21,10 @@ const cli = meow( $ get-unity [options] Options - ${chalk.yellow('--file, -f')} Search file for Unity version number. - ${chalk.yellow('--help, -h')} Display this help message. - ${chalk.yellow('--version, -v')} Display the current installed version. + ${chalk.yellow('--file, -f')} Search file for Unity version number. + ${chalk.yellow('--offline, -o')} Prevent request to update local cache of editor versions. + ${chalk.yellow('--help, -h')} Display this help message. + ${chalk.yellow('--version, -v')} Display the current installed version. `, { 'flags': { @@ -34,6 +37,11 @@ const cli = meow( 'default': false, 'type': 'boolean' }, + 'offline': { + 'alias': 'o', + 'default': false, + 'type': 'boolean' + }, 'version': { 'alias': 'v', 'default': false, @@ -48,6 +56,11 @@ const osKeyMap = { 'Windows_NT': 'win64' }; +const EDITOR_INSTALLERS_FILE_PATH = join( + __dirname, + '../data/editor-installers.json' +); + updateNotifier({pkg}).notify(); if (cli.flags.file) { @@ -69,5 +82,26 @@ if (cli.flags.file) { } -getUnityUrls(cli.input[0]).then(urls => - process.stdout.write(`${urls[osKeyMap[os.type()]]}`)); +if (cli.flags.offline) { + + getUnityUrls( + cli.input[0], + EDITOR_INSTALLERS_FILE_PATH + ).then(urls => + process.stdout.write(`${urls[osKeyMap[os.type()]]}`)); + +} else { + + updateEditorInstallers(EDITOR_INSTALLERS_FILE_PATH) + .catch(({message}) => { + + process.stderr.write(`${chalk.red('Error:')} ${message}`); + + }) + .then(() => getUnityUrls( + cli.input[0], + EDITOR_INSTALLERS_FILE_PATH + )) + .then(urls => process.stdout.write(`${urls[osKeyMap[os.type()]]}`)); + +} diff --git a/data/editor-installers.json b/data/editor-installers.json index 5c8af66..9208e21 100644 --- a/data/editor-installers.json +++ b/data/editor-installers.json @@ -1,7 +1,7 @@ { "2019.2.11f1": { "mac": "https://download.unity3d.com/download_unity/5f859a4cfee5/MacEditorInstaller/Unity-2019.2.11f1.pkg", - "win64": "https://netstorage.unity3d.com/unity/5f859a4cfee5/Windows64EditorInstaller/UnitySetup64-2019.2.11f1.exe" + "win64": "https://download.unity3d.com/download_unity/5f859a4cfee5/Windows64EditorInstaller/UnitySetup64-2019.2.11f1.exe" }, "2019.2.10f1": { "mac": "https://download.unity3d.com/download_unity/923acd2d43aa/MacEditorInstaller/Unity-2019.2.10f1.pkg", @@ -541,42 +541,34 @@ }, "5.6.7f1": { "mac": "https://download.unity3d.com/download_unity/e80cc3114ac1/MacEditorInstaller/Unity-5.6.7f1.pkg", - "win64": "https://download.unity3d.com/download_unity/e80cc3114ac1/Windows64EditorInstaller/UnitySetup64-5.6.7f1.exe", - "win32": "https://download.unity3d.com/download_unity/e80cc3114ac1/Windows32EditorInstaller/UnitySetup32-5.6.7f1.exe" + "win64": "https://download.unity3d.com/download_unity/e80cc3114ac1/Windows64EditorInstaller/UnitySetup64-5.6.7f1.exe" }, "5.6.6f2": { "mac": "https://download.unity3d.com/download_unity/6bac21139588/MacEditorInstaller/Unity-5.6.6f2.pkg", - "win64": "https://download.unity3d.com/download_unity/6bac21139588/Windows64EditorInstaller/UnitySetup64-5.6.6f2.exe", - "win32": "https://download.unity3d.com/download_unity/6bac21139588/Windows32EditorInstaller/UnitySetup32-5.6.6f2.exe" + "win64": "https://download.unity3d.com/download_unity/6bac21139588/Windows64EditorInstaller/UnitySetup64-5.6.6f2.exe" }, "5.6.5f1": { "mac": "https://download.unity3d.com/download_unity/2cac56bf7bb6/MacEditorInstaller/Unity-5.6.5f1.pkg", - "win64": "https://download.unity3d.com/download_unity/2cac56bf7bb6/Windows64EditorInstaller/UnitySetup64-5.6.5f1.exe", - "win32": "https://download.unity3d.com/download_unity/2cac56bf7bb6/Windows32EditorInstaller/UnitySetup32-5.6.5f1.exe" + "win64": "https://download.unity3d.com/download_unity/2cac56bf7bb6/Windows64EditorInstaller/UnitySetup64-5.6.5f1.exe" }, "5.6.4f1": { "mac": "https://download.unity3d.com/download_unity/ac7086b8d112/MacEditorInstaller/Unity-5.6.4f1.pkg", - "win64": "https://download.unity3d.com/download_unity/ac7086b8d112/Windows64EditorInstaller/UnitySetup64-5.6.4f1.exe", - "win32": "https://download.unity3d.com/download_unity/ac7086b8d112/Windows32EditorInstaller/UnitySetup32-5.6.4f1.exe" + "win64": "https://download.unity3d.com/download_unity/ac7086b8d112/Windows64EditorInstaller/UnitySetup64-5.6.4f1.exe" }, "5.6.3f1": { "mac": "https://download.unity3d.com/download_unity/d3101c3b8468/MacEditorInstaller/Unity-5.6.3f1.pkg", - "win64": "https://download.unity3d.com/download_unity/d3101c3b8468/Windows64EditorInstaller/UnitySetup64-5.6.3f1.exe", - "win32": "https://download.unity3d.com/download_unity/d3101c3b8468/Windows32EditorInstaller/UnitySetup32-5.6.3f1.exe" + "win64": "https://download.unity3d.com/download_unity/d3101c3b8468/Windows64EditorInstaller/UnitySetup64-5.6.3f1.exe" }, "5.6.2f1": { "mac": "https://download.unity3d.com/download_unity/a2913c821e27/MacEditorInstaller/Unity-5.6.2f1.pkg", - "win64": "https://download.unity3d.com/download_unity/a2913c821e27/Windows64EditorInstaller/UnitySetup64-5.6.2f1.exe", - "win32": "https://download.unity3d.com/download_unity/a2913c821e27/Windows32EditorInstaller/UnitySetup32-5.6.2f1.exe" + "win64": "https://download.unity3d.com/download_unity/a2913c821e27/Windows64EditorInstaller/UnitySetup64-5.6.2f1.exe" }, "5.6.1f1": { "mac": "https://download.unity3d.com/download_unity/2860b30f0b54/MacEditorInstaller/Unity-5.6.1f1.pkg", - "win64": "https://download.unity3d.com/download_unity/2860b30f0b54/Windows64EditorInstaller/UnitySetup64-5.6.1f1.exe", - "win32": "https://download.unity3d.com/download_unity/2860b30f0b54/Windows32EditorInstaller/UnitySetup32-5.6.1f1.exe" + "win64": "https://download.unity3d.com/download_unity/2860b30f0b54/Windows64EditorInstaller/UnitySetup64-5.6.1f1.exe" }, "5.6.0f3": { "mac": "https://download.unity3d.com/download_unity/497a0f351392/MacEditorInstaller/Unity-5.6.0f3.pkg", - "win64": "https://download.unity3d.com/download_unity/497a0f351392/Windows64EditorInstaller/UnitySetup64-5.6.0f3.exe", - "win32": "https://download.unity3d.com/download_unity/497a0f351392/Windows32EditorInstaller/UnitySetup32-5.6.0f3.exe" + "win64": "https://download.unity3d.com/download_unity/497a0f351392/Windows64EditorInstaller/UnitySetup64-5.6.0f3.exe" } -} +} \ No newline at end of file diff --git a/index.js b/index.js index 44c48bf..6fb37b5 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,13 @@ +const checkCacheExpiry = require('./utils/check-cache-expiry'); +const fetchWithLocalCache = require('./utils/fetch-with-local-cache'); const getUnityUrls = require('./lib/get-unity-urls'); const parsers = require('./lib/parsers'); +const updateEditorInstallers = require('./lib/update-editor-installers'); module.exports = { + checkCacheExpiry, + fetchWithLocalCache, getUnityUrls, - parsers + parsers, + updateEditorInstallers }; diff --git a/lib/get-unity-urls.js b/lib/get-unity-urls.js index 0e1dbfd..5049d6c 100644 --- a/lib/get-unity-urls.js +++ b/lib/get-unity-urls.js @@ -1,23 +1,35 @@ -const editorInstallers = require('../data/editor-installers.json'); +const fs = require('fs'); +const {join} = require('path'); +const {promisify} = require('util'); -const getUnityUrls = (filter = '') => - new Promise(resolve => { +const readFile = promisify(fs.readFile); - const versions = Object.keys(editorInstallers); +const EDITOR_INSTALLERS_FILE_PATH = join( + __dirname, + '../data/editor-installers.json' +); - const [latest] = versions; +const getUnityUrls = (filter = '', filePath = EDITOR_INSTALLERS_FILE_PATH) => + readFile(filePath) + .then(data => JSON.parse(data)) + .then(editorInstallers => { - const match = - versions.find(version => - version.match(new RegExp( - `^${filter.replace( - 'x', - '[0-9]+' - )}`, - 'u' - ))) || latest; + const versions = Object.keys(editorInstallers); - resolve(editorInstallers[match]); + const [latest] = versions; + + const match = + versions.find(version => + version.match(new RegExp( + `^${filter.replace( + 'x', + '[0-9]+' + )}`, + 'u' + ))) || latest; + + return editorInstallers[match]; + + }); - }); module.exports = getUnityUrls; diff --git a/lib/update-editor-installers.js b/lib/update-editor-installers.js new file mode 100644 index 0000000..8980004 --- /dev/null +++ b/lib/update-editor-installers.js @@ -0,0 +1,85 @@ +const fs = require('fs'); +const {join} = require('path'); +const {promisify} = require('util'); + +const writeFile = promisify(fs.writeFile); + +const {JSDOM} = require('jsdom'); + +const checkCacheExpiry = require('../utils/check-cache-expiry'); +const fetchWithLocalCache = require('../utils/fetch-with-local-cache'); + +const ARCHIVE_FILE_PATH = join( + __dirname, + '../temp/archive.html' +); + +const EDITOR_INSTALLERS_FILE_PATH = join( + __dirname, + '../data/editor-installers.json' +); + +const JSON_TAB_WIDTH = 2; + +const CACHE_TTL = 3600000; + +const parseVersionFromUnityArchive = body => { + + const {document} = new JSDOM(body).window; + + return [].slice + .call(document.querySelectorAll('a[href^="unityhub://"]')) + .reduce( + (acc, elem) => { + + const link = elem.getAttribute('href'); + + const [ + version, + hash + ] = link.replace( + 'unityhub://', + '' + ).split('/'); + + acc[version] = { + 'mac': `https://download.unity3d.com/download_unity/${hash}/MacEditorInstaller/Unity-${version}.pkg`, + 'win64': `https://download.unity3d.com/download_unity/${hash}/Windows64EditorInstaller/UnitySetup64-${version}.exe` + }; + + return acc; + + }, + {} + ); + +}; + +const updateEditorInstallers = (filePath = EDITOR_INSTALLERS_FILE_PATH) => + checkCacheExpiry( + filePath, + CACHE_TTL + ) + .catch(() => + fetchWithLocalCache( + 'https://unity3d.com/get-unity/download/archive', + ARCHIVE_FILE_PATH, + CACHE_TTL + ) + .then(parseVersionFromUnityArchive) + .then(data => + writeFile( + filePath, + JSON.stringify( + data, + null, + JSON_TAB_WIDTH + ) + ))) + .catch(() => { + + throw new Error('There was an error fetching the latest versions from unity3d.com'); + + }); + +module.exports = updateEditorInstallers; diff --git a/package.json b/package.json index 767533a..11dc77f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "license": "MIT", "dependencies": { "chalk": "2.4.2", + "jsdom": "15.2.1", "meow": "5.0.0", + "request": "2.88.0", "update-notifier": "3.0.1" }, "devDependencies": { diff --git a/utils/check-cache-expiry.js b/utils/check-cache-expiry.js new file mode 100644 index 0000000..7064e42 --- /dev/null +++ b/utils/check-cache-expiry.js @@ -0,0 +1,17 @@ +const fs = require('fs'); +const {promisify} = require('util'); + +const stat = promisify(fs.stat); + +const checkCacheExpiry = (path, ttl) => + stat(path).then(({mtime}) => { + + if (new Date(mtime).getTime() + ttl < Date.now()) { + + throw new Error('Cache has expired.'); + + } + + }); + +module.exports = checkCacheExpiry; diff --git a/utils/fetch-with-local-cache.js b/utils/fetch-with-local-cache.js new file mode 100644 index 0000000..05de8ce --- /dev/null +++ b/utils/fetch-with-local-cache.js @@ -0,0 +1,32 @@ +const fs = require('fs'); +const {dirname} = require('path'); +const {promisify} = require('util'); + +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +const mkdirp = promisify(require('mkdirp')); +const request = promisify(require('request')); + +const checkCacheExpiry = require('./check-cache-expiry'); + +const DEFAULT_CACHE_TTL = 3600000; + +const fetchWithLocalCache = (url, localCachePath, ttl = DEFAULT_CACHE_TTL) => + checkCacheExpiry( + localCachePath, + ttl + ) + .then(() => readFile( + localCachePath, + 'utf8' + )) + .catch(() => + request(url).then(({body}) => + mkdirp(dirname(localCachePath)).then(() => + writeFile( + localCachePath, + body + ).then(() => body)))); + +module.exports = fetchWithLocalCache;