assetguard.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. /* Requirements */
  2. const fs = require('fs')
  3. const request = require('request')
  4. const path = require('path')
  5. const mkpath = require('mkdirp');
  6. const async = require('async')
  7. const crypto = require('crypto')
  8. const EventEmitter = require('events');
  9. const {remote} = require('electron')
  10. /* Classes */
  11. class Asset{
  12. constructor(id, hash, size, from, to){
  13. this.id = id
  14. this.hash = hash
  15. this.size = size
  16. this.from = from
  17. this.to = to
  18. }
  19. }
  20. class Library extends Asset{
  21. static mojangFriendlyOS(){
  22. const opSys = process.platform
  23. if (opSys === 'darwin') {
  24. return 'osx';
  25. } else if (opSys === 'win32'){
  26. return 'windows';
  27. } else if (opSys === 'linux'){
  28. return 'linux';
  29. } else {
  30. return 'unknown_os';
  31. }
  32. }
  33. static validateRules(rules){
  34. if(rules == null) return true
  35. let result = true
  36. rules.forEach(function(rule){
  37. const action = rule['action']
  38. const osProp = rule['os']
  39. if(action != null){
  40. if(osProp != null){
  41. const osName = osProp['name']
  42. const osMoj = Library.mojangFriendlyOS()
  43. if(action === 'allow'){
  44. result = osName === osMoj
  45. return
  46. } else if(action === 'disallow'){
  47. result = osName !== osMoj
  48. return
  49. }
  50. }
  51. }
  52. })
  53. return result
  54. }
  55. }
  56. class DLTracker {
  57. constructor(dlqueue, dlsize){
  58. this.dlqueue = dlqueue
  59. this.dlsize = dlsize
  60. }
  61. }
  62. class AssetGuard extends EventEmitter{
  63. constructor(){
  64. super()
  65. this.totaldlsize = 0;
  66. this.progress = 0;
  67. this.assets = new DLTracker([], 0)
  68. this.libraries = new DLTracker([], 0)
  69. this.files = new DLTracker([], 0)
  70. }
  71. }
  72. //Instance of AssetGuard
  73. const instance = new AssetGuard()
  74. /* Utility Functions */
  75. validateLocal = function(filePath, algo, hash){
  76. if(fs.existsSync(filePath)){
  77. let fileName = path.basename(filePath)
  78. let shasum = crypto.createHash(algo)
  79. let content = fs.readFileSync(filePath)
  80. shasum.update(content)
  81. let calcdhash = shasum.digest('hex')
  82. return calcdhash === hash
  83. }
  84. return false;
  85. }
  86. /* Validation Functions */
  87. /**
  88. * Load version asset index.
  89. */
  90. loadVersionData = function(version, basePath, force = false){
  91. return new Promise(function(fulfill, reject){
  92. const name = version + '.json'
  93. const url = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
  94. const versionPath = path.join(basePath, 'versions', version)
  95. const versionFile = path.join(versionPath, name)
  96. if(!fs.existsSync(versionFile) || force){
  97. //This download will never be tracked as it's essential and trivial.
  98. request.head(url, function(err, res, body){
  99. console.log('Preparing download of ' + version + ' assets.')
  100. mkpath.sync(versionPath)
  101. const stream = request(url).pipe(fs.createWriteStream(versionFile))
  102. stream.on('finish', function(){
  103. fulfill(JSON.parse(fs.readFileSync(versionFile)))
  104. })
  105. })
  106. } else {
  107. fulfill(JSON.parse(fs.readFileSync(versionFile)))
  108. }
  109. })
  110. }
  111. /**
  112. * Public asset validation method.
  113. */
  114. validateAssets = function(versionData, basePath, force = false){
  115. return new Promise(function(fulfill, reject){
  116. assetChainIndexData(versionData, basePath, force).then(() => {
  117. fulfill()
  118. })
  119. })
  120. }
  121. //Chain the asset tasks to provide full async. The below functions are private.
  122. assetChainIndexData = function(versionData, basePath, force = false){
  123. return new Promise(function(fulfill, reject){
  124. //Asset index constants.
  125. const assetIndex = versionData.assetIndex
  126. const name = assetIndex.id + '.json'
  127. const indexPath = path.join(basePath, 'assets', 'indexes')
  128. const assetIndexLoc = path.join(indexPath, name)
  129. let data = null
  130. if(!fs.existsSync(assetIndexLoc) || force){
  131. console.log('Downloading ' + versionData.id + ' asset index.')
  132. mkpath.sync(indexPath)
  133. const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc))
  134. stream.on('finish', function() {
  135. data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
  136. assetChainValidateAssets(versionData, basePath, data).then(() => {
  137. fulfill()
  138. })
  139. })
  140. } else {
  141. data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
  142. assetChainValidateAssets(versionData, basePath, data).then(() => {
  143. fulfill()
  144. })
  145. }
  146. })
  147. }
  148. assetChainValidateAssets = function(versionData, basePath, indexData){
  149. return new Promise(function(fulfill, reject){
  150. //Asset constants
  151. const resourceURL = 'http://resources.download.minecraft.net/'
  152. const localPath = path.join(basePath, 'assets')
  153. const indexPath = path.join(localPath, 'indexes')
  154. const objectPath = path.join(localPath, 'objects')
  155. const assetDlQueue = []
  156. let dlSize = 0;
  157. //const objKeys = Object.keys(data.objects)
  158. async.forEachOfLimit(indexData.objects, 10, function(value, key, cb){
  159. const hash = value.hash
  160. const assetName = path.join(hash.substring(0, 2), hash)
  161. const urlName = hash.substring(0, 2) + "/" + hash
  162. const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName))
  163. if(!validateLocal(ast.to, 'sha1', ast.hash)){
  164. dlSize += (ast.size*1)
  165. assetDlQueue.push(ast)
  166. }
  167. cb()
  168. }, function(err){
  169. instance.assets = new DLTracker(assetDlQueue, dlSize)
  170. instance.totaldlsize += dlSize
  171. fulfill()
  172. })
  173. })
  174. }
  175. /**
  176. * Public library validation method.
  177. */
  178. validateLibraries = function(versionData, basePath){
  179. return new Promise(function(fulfill, reject){
  180. const libArr = versionData.libraries
  181. const libPath = path.join(basePath, 'libraries')
  182. const libDlQueue = []
  183. let dlSize = 0
  184. //Check validity of each library. If the hashs don't match, download the library.
  185. async.eachLimit(libArr, 5, function(lib, cb){
  186. if(Library.validateRules(lib.rules)){
  187. let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
  188. const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
  189. if(!validateLocal(libItm.to, 'sha1', libItm.hash)){
  190. dlSize += (libItm.size*1)
  191. libDlQueue.push(libItm)
  192. }
  193. }
  194. cb()
  195. }, function(err){
  196. instance.libraries = new DLTracker(libDlQueue, dlSize)
  197. instance.totaldlsize += dlSize
  198. fulfill()
  199. })
  200. })
  201. }
  202. runQueue = function(){
  203. this.progress = 0;
  204. let win = remote.getCurrentWindow()
  205. //Start asset download
  206. let assetacc = 0;
  207. const concurrentAssetDlQueue = instance.assets.dlqueue.slice(0)
  208. async.eachLimit(concurrentAssetDlQueue, 20, function(asset, cb){
  209. mkpath.sync(path.join(asset.to, ".."))
  210. let req = request(asset.from)
  211. let writeStream = fs.createWriteStream(asset.to)
  212. req.pipe(writeStream)
  213. req.on('data', function(chunk){
  214. instance.progress += chunk.length
  215. assetacc += chunk.length
  216. instance.emit('assetdata', assetacc)
  217. console.log('Asset Progress', assetacc/instance.assets.dlsize)
  218. win.setProgressBar(instance.progress/instance.totaldlsize)
  219. //console.log('Total Progress', instance.progress/instance.totaldlsize)
  220. })
  221. writeStream.on('close', cb)
  222. }, function(err){
  223. if(err){
  224. instance.emit('asseterr')
  225. console.log('An asset failed to process');
  226. } else {
  227. instance.emit('assetdone')
  228. console.log('All assets have been processed successfully')
  229. }
  230. instance.totaldlsize -= instance.assets.dlsize
  231. instance.assets = new DLTracker([], 0)
  232. win.setProgressBar(-1)
  233. })
  234. //Start library download
  235. let libacc = 0
  236. const concurrentLibraryDlQueue = instance.libraries.dlqueue.slice(0)
  237. async.eachLimit(concurrentLibraryDlQueue, 1, function(lib, cb){
  238. mkpath.sync(path.join(lib.to, '..'))
  239. let req = request(lib.from)
  240. let writeStream = fs.createWriteStream(lib.to)
  241. req.pipe(writeStream)
  242. req.on('data', function(chunk){
  243. instance.progress += chunk.length
  244. libacc += chunk.length
  245. instance.emit('librarydata', libacc)
  246. console.log('Library Progress', libacc/instance.libraries.dlsize)
  247. win.setProgressBar(instance.progress/instance.totaldlsize)
  248. })
  249. writeStream.on('close', cb)
  250. }, function(err){
  251. if(err){
  252. instance.emit('libraryerr')
  253. console.log('A library failed to process');
  254. } else {
  255. instance.emit('librarydone')
  256. console.log('All libraries have been processed successfully');
  257. }
  258. instance.totaldlsize -= instance.libraries.dlsize
  259. instance.libraries = new DLTracker([], 0)
  260. win.setProgressBar(-1)
  261. })
  262. }
  263. module.exports = {
  264. loadVersionData,
  265. validateAssets,
  266. validateLibraries,
  267. runQueue,
  268. instance
  269. }