assetdownload.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. const fs = require('fs')
  2. const request = require('request')
  3. const path = require('path')
  4. const mkpath = require('mkdirp');
  5. const async = require('async')
  6. const crypto = require('crypto')
  7. function Asset(from, to, size, hash){
  8. this.from = from
  9. this.to = to
  10. this.size = size
  11. this.hash = hash
  12. }
  13. function AssetIndex(id, sha1, size, url, totalSize){
  14. this.id = id
  15. this.sha1 = sha1
  16. this.size = size
  17. this.url = url
  18. this.totalSize = totalSize
  19. }
  20. /**
  21. * This function will download the version index data and read it into a Javascript
  22. * Object. This object will then be returned.
  23. */
  24. exports.parseVersionData = function(version, basePath){
  25. const name = version + '.json'
  26. const baseURL = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
  27. const versionPath = path.join(basePath, 'versions', version)
  28. return new Promise(function(fulfill, reject){
  29. request.head(baseURL, function(err, res, body){
  30. console.log('Preparing download of ' + version + ' assets.')
  31. mkpath.sync(versionPath)
  32. const stream = request(baseURL).pipe(fs.createWriteStream(path.join(versionPath, name)))
  33. stream.on('finish', function(){
  34. fulfill(JSON.parse(fs.readFileSync(path.join(versionPath, name))))
  35. })
  36. })
  37. })
  38. }
  39. /**
  40. * Download the client for version. This file is 'client.jar' although
  41. * it must be renamed to '{version}'.jar.
  42. */
  43. exports.downloadClient = function(versionData, basePath){
  44. const dls = versionData['downloads']
  45. const clientData = dls['client']
  46. const url = clientData['url']
  47. const size = clientData['size']
  48. const version = versionData['id']
  49. const sha1 = clientData['sha1']
  50. const targetPath = path.join(basePath, 'versions', version)
  51. const targetFile = version + '.jar'
  52. if(!validateLocalIntegrity(path.join(targetPath, targetFile), 'sha1', sha1)){
  53. request.head(url, function(err, res, body){
  54. console.log('Downloading ' + version + ' client..')
  55. mkpath.sync(targetPath)
  56. const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, targetFile)))
  57. stream.on('finish', function(){
  58. console.log('Finished downloading ' + version + ' client.')
  59. })
  60. })
  61. }
  62. }
  63. exports.downloadLogConfig = function(versionData, basePath){
  64. const logging = versionData['logging']
  65. const client = logging['client']
  66. const file = client['file']
  67. const version = versionData['id']
  68. const sha1 = file['sha1']
  69. const targetPath = path.join(basePath, 'assets', 'log_configs')
  70. const name = file['id']
  71. const url = file['url']
  72. if(!validateLocalIntegrity(path.join(targetPath, name), 'sha1', sha1)){
  73. request.head(url, function(err, res, body){
  74. console.log('Downloading ' + version + ' log config..')
  75. mkpath.sync(targetPath)
  76. const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, name)))
  77. stream.on('finish', function(){
  78. console.log('Finished downloading ' + version + ' log config..')
  79. })
  80. })
  81. }
  82. }
  83. exports.downloadLibraries = function(versionData, basePath){
  84. const libArr = versionData['libraries']
  85. const libPath = path.join(basePath, 'libraries')
  86. async.eachLimit(libArr, 1, function(lib, cb){
  87. if(validateRules(lib['rules'])){
  88. if(lib['natives'] == null){
  89. const dlInfo = lib['downloads']
  90. const artifact = dlInfo['artifact']
  91. const sha1 = artifact['sha1']
  92. const libSize = artifact['size']
  93. const to = path.join(libPath, artifact['path'])
  94. const from = artifact['url']
  95. if(!validateLocalIntegrity(to, 'sha1', sha1)){
  96. mkpath.sync(path.join(to, ".."))
  97. let req = request(from)
  98. let writeStream = fs.createWriteStream(to)
  99. req.pipe(writeStream)
  100. let acc = 0;
  101. req.on('data', function(chunk){
  102. acc += chunk.length
  103. //console.log('Progress', acc/libSize)
  104. })
  105. writeStream.on('close', function(){
  106. cb()
  107. })
  108. } else {
  109. cb()
  110. }
  111. } else {
  112. const natives = lib['natives']
  113. const opSys = mojangFriendlyOS()
  114. const indexId = natives[opSys]
  115. const dlInfo = lib['downloads']
  116. const classifiers = dlInfo['classifiers']
  117. const artifact = classifiers[indexId]
  118. const libSize = artifact['size']
  119. const to = path.join(libPath, artifact['path'])
  120. const from = artifact['url']
  121. const sha1 = artifact['sha1']
  122. if(!validateLocalIntegrity(to, 'sha1', sha1)){
  123. mkpath.sync(path.join(to, ".."))
  124. let req = request(from)
  125. let writeStream = fs.createWriteStream(to)
  126. req.pipe(writeStream)
  127. let acc = 0;
  128. req.on('data', function(chunk){
  129. acc += chunk.length
  130. console.log('Progress', acc/libSize)
  131. })
  132. writeStream.on('close', function(){
  133. cb()
  134. })
  135. } else {
  136. cb()
  137. }
  138. }
  139. } else {
  140. cb()
  141. }
  142. }, function(err){
  143. if(err){
  144. console.log('A library failed to process');
  145. } else {
  146. console.log('All libraries have been processed successfully');
  147. }
  148. })
  149. }
  150. /**
  151. * Given an index url, this function will asynchonously download the
  152. * assets associated with that version.
  153. */
  154. exports.downloadAssets = function(versionData, basePath){
  155. //Asset index constants.
  156. const assetIndex = versionData['assetIndex']
  157. const indexURL = assetIndex['url']
  158. const datasize = assetIndex['totalSize']
  159. const gameVersion = versionData['id']
  160. const assetVersion = assetIndex['id']
  161. const name = assetVersion + '.json'
  162. //Asset constants
  163. const resourceURL = 'http://resources.download.minecraft.net/'
  164. const localPath = path.join(basePath, 'assets')
  165. const indexPath = path.join(localPath, 'indexes')
  166. const objectPath = path.join(localPath, 'objects')
  167. request.head(indexURL, function (err, res, body) {
  168. console.log('Downloading ' + gameVersion + ' asset index.')
  169. mkpath.sync(indexPath)
  170. const stream = request(indexURL).pipe(fs.createWriteStream(path.join(indexPath, name)))
  171. stream.on('finish', function() {
  172. const data = JSON.parse(fs.readFileSync(path.join(indexPath, name), 'utf-8'))
  173. const assetArr = []
  174. Object.keys(data['objects']).forEach(function(key, index){
  175. const ob = data['objects'][key]
  176. const hash = String(ob['hash'])
  177. const assetName = path.join(hash.substring(0, 2), hash)
  178. const urlName = hash.substring(0, 2) + "/" + hash
  179. const ast = new Asset(resourceURL + urlName, path.join(objectPath, assetName), ob['size'], hash)
  180. assetArr.push(ast)
  181. })
  182. let acc = 0;
  183. async.eachLimit(assetArr, 5, function(asset, cb){
  184. mkpath.sync(path.join(asset.to, ".."))
  185. if(!validateLocalIntegrity(asset.to, 'sha1', asset.hash)){
  186. let req = request(asset.from)
  187. let writeStream = fs.createWriteStream(asset.to)
  188. req.pipe(writeStream)
  189. req.on('data', function(chunk){
  190. acc += chunk.length
  191. //console.log('Progress', acc/datasize)
  192. })
  193. writeStream.on('close', function(){
  194. cb()
  195. })
  196. } else {
  197. cb()
  198. }
  199. }, function(err){
  200. if(err){
  201. console.log('An asset failed to process');
  202. } else {
  203. console.log('All assets have been processed successfully');
  204. }
  205. })
  206. })
  207. })
  208. }
  209. validateLocalIntegrity = function(filePath, algo, hash){
  210. if(fs.existsSync(filePath)){
  211. let fileName = path.basename(filePath)
  212. console.log('Validating integrity of local file', fileName)
  213. let shasum = crypto.createHash(algo)
  214. let content = fs.readFileSync(filePath)
  215. shasum.update(content)
  216. let localhash = shasum.digest('hex')
  217. if(localhash === hash){
  218. console.log('Hash value of ' + fileName + ' matches the index hash, woo!')
  219. return true
  220. } else {
  221. console.log('Hash value of ' + fileName + ' (' + localhash + ')' + ' does not match the index hash. Redownloading..')
  222. return false
  223. }
  224. }
  225. return false;
  226. }
  227. validateRules = function(rules){
  228. if(rules == null) return true
  229. let result = true
  230. rules.forEach(function(rule){
  231. const action = rule['action']
  232. const osProp = rule['os']
  233. if(action != null){
  234. if(osProp != null){
  235. const osName = osProp['name']
  236. const osMoj = mojangFriendlyOS()
  237. if(action === 'allow'){
  238. result = osName === osMoj
  239. return
  240. } else if(action === 'disallow'){
  241. result = osName !== osMoj
  242. return
  243. }
  244. }
  245. }
  246. })
  247. return result
  248. }
  249. mojangFriendlyOS = function(){
  250. const opSys = process.platform
  251. if (opSys === 'darwin') {
  252. return 'osx';
  253. } else if (opSys === 'win32'){
  254. return 'windows';
  255. } else if (opSys === 'linux'){
  256. return 'linux';
  257. } else {
  258. return 'unknown_os';
  259. }
  260. }