├── .gitignore ├── class ├── collection.lua ├── download.lua ├── framework.lua ├── game.lua ├── image.lua ├── mirror.lua ├── release.lua └── source.lua ├── init.lua ├── json.lua ├── lang.lua ├── license.txt ├── readme.md └── util.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /class/collection.lua: -------------------------------------------------------------------------------- 1 | local collection = {} 2 | 3 | function collection:all() 4 | return self._data 5 | end 6 | 7 | function collection:add(val) 8 | table.insert(self._data,val) 9 | return val 10 | end 11 | 12 | function collection:remove(val) 13 | for i,v in pairs(self._data) do 14 | if v == val then 15 | return table.remove(self._data,i) 16 | end 17 | end 18 | return false 19 | end 20 | 21 | function collection:empty() 22 | self._data = {} 23 | return self._data 24 | end 25 | 26 | function collection:isEmpty() 27 | return #self._data == 0 28 | end 29 | 30 | function collection:count() 31 | return #self._data 32 | end 33 | 34 | function collection:has(val) 35 | for _,v in pairs(self._data) do 36 | if v == val then 37 | return true 38 | end 39 | end 40 | return false 41 | end 42 | 43 | function collection:find(f) 44 | local found = {} 45 | for _,v in pairs(self._data) do 46 | if f(v) then 47 | table.insert(found,v) 48 | end 49 | end 50 | return found 51 | end 52 | 53 | -- LuaClassGen pregenerated functions 54 | 55 | function collection.new(init) 56 | init = init or {} 57 | local self={} 58 | self.all=collection.all 59 | self.add=collection.add 60 | self.remove=collection.remove 61 | self.empty=collection.empty 62 | self.isEmpty=collection.isEmpty 63 | self.count=collection.count 64 | self.has=collection.has 65 | self.find=collection.find 66 | self._data=init.data or {} 67 | return self 68 | end 69 | 70 | return collection 71 | -------------------------------------------------------------------------------- /class/download.lua: -------------------------------------------------------------------------------- 1 | local download = {} 2 | 3 | function download:download() 4 | self:setStatus(vapor.status.download) 5 | return self 6 | end 7 | 8 | function download:getFilename() 9 | return self._filenamePrefix.."_"..self._identifier..".dat" 10 | end 11 | 12 | function download:getPath() 13 | if self:getUnzip() then 14 | return "unzipped-"..self._filenamePrefix.."_"..self._identifier 15 | else 16 | return "" 17 | end 18 | end 19 | 20 | function download:update() 21 | 22 | if self:getIdentifier() then 23 | 24 | if love.filesystem.exists(self:getFilename()) then 25 | self:setStatus(vapor.status.ready) 26 | end 27 | 28 | if self:getStatus() == vapor.status.downloaded then 29 | -- TODO: perhform hash check 30 | 31 | love.filesystem.write(self:getFilename(),self._data) 32 | print("data written to ",self:getFilename()) 33 | self._data = nil 34 | 35 | print(self._unzip) 36 | if self._unzip then 37 | print("UNZIPPIN'") 38 | vapor.util.unzip(self:getFilename()) 39 | end 40 | 41 | self:setStatus(vapor.status.ready) 42 | end 43 | 44 | if self:getStatus() == vapor.status.download then 45 | 46 | if self._uri then 47 | 48 | if self._uri:sub(0,7) == "http://" then 49 | -- TODO: make async 50 | self:setStatus(vapor.status.downloading) 51 | print("Downloading URI: "..self._uri) 52 | local b, c, h = vapor.util.http.request(self._uri) 53 | if c == 200 then 54 | self:setStatus(vapor.status.downloaded) 55 | self._data = b 56 | else 57 | self:setStatus(vapor.status.fail) 58 | end 59 | elseif self._uri:sub(0,8) == "https://" then 60 | -- TODO: add https support 61 | self:setStatus(vapor.status.fail) 62 | elseif self._uri:sub(0,7) == "file://" then 63 | -- TODO: add local file support 64 | self:setStatus(vapor.status.fail) 65 | else -- uh, wat 66 | self:setStatus(vapor.status.fail) 67 | end 68 | 69 | else 70 | print("download does not have uri set.") 71 | end 72 | 73 | end 74 | 75 | end 76 | 77 | end 78 | 79 | function download:clear() 80 | end 81 | 82 | -- LuaClassGen pregenerated functions 83 | 84 | function download.new(init) 85 | init = init or {} 86 | 87 | local self={} 88 | self.download=download.download 89 | self.getFilename=download.getFilename 90 | self.getPath=download.getPath 91 | self.update=download.update 92 | self.clear=download.clear 93 | self._identifier=init.identifier 94 | self.getIdentifier=download.getIdentifier 95 | self.setIdentifier=download.setIdentifier 96 | self._state=init.state 97 | self.getState=download.getState 98 | self.setState=download.setState 99 | self._hash=init.hash 100 | self.getHash=download.getHash 101 | self.setHash=download.setHash 102 | self._size=init.size 103 | self.getSize=download.getSize 104 | self.setSize=download.setSize 105 | self._data=init.data 106 | self.getData=download.getData 107 | self.setData=download.setData 108 | self._unzip=init.unzip 109 | self.getUnzip=download.getUnzip 110 | self.setUnzip=download.setUnzip 111 | self._location=init.location 112 | self.getLocation=download.getLocation 113 | self.setLocation=download.setLocation 114 | self._status=init.status or vapor.status.uninitialized 115 | self.getStatus=download.getStatus 116 | self.setStatus=download.setStatus 117 | self._uri=init.uri 118 | self.getUri=download.getUri 119 | self.setUri=download.setUri 120 | self._filenamePrefix=init.filenamePrefix 121 | self.getFilenamePrefix=download.getFilenamePrefix 122 | self.setFilenamePrefix=download.setFilenamePrefix 123 | self._exec=init.exec 124 | self.getExec=download.getExec 125 | self.setExec=download.setExec 126 | return self 127 | end 128 | 129 | function download:getIdentifier() 130 | return self._identifier 131 | end 132 | 133 | function download:setIdentifier(val) 134 | self._identifier=val 135 | end 136 | 137 | function download:getState() 138 | return self._state 139 | end 140 | 141 | function download:setState(val) 142 | self._state=val 143 | end 144 | 145 | function download:getHash() 146 | return self._hash 147 | end 148 | 149 | function download:setHash(val) 150 | self._hash=val 151 | end 152 | 153 | function download:getSize() 154 | return self._size 155 | end 156 | 157 | function download:setSize(val) 158 | self._size=val 159 | end 160 | 161 | function download:getData() 162 | return self._data 163 | end 164 | 165 | function download:setData(val) 166 | self._data=val 167 | end 168 | 169 | function download:getUnzip() 170 | return self._unzip 171 | end 172 | 173 | function download:setUnzip(val) 174 | self._unzip=val 175 | end 176 | 177 | function download:getLocation() 178 | return self._location 179 | end 180 | 181 | function download:setLocation(val) 182 | self._location=val 183 | end 184 | 185 | function download:getStatus() 186 | return self._status 187 | end 188 | 189 | function download:setStatus(val) 190 | assert(type(val)=="number") 191 | -- TODO: remove for release 192 | --[[ 193 | for i,v in pairs(vapor.status) do 194 | if v == val then 195 | print("Setting status: "..i) 196 | end 197 | end 198 | --]] 199 | self._status=val 200 | end 201 | 202 | function download:getUri() 203 | return self._uri 204 | end 205 | 206 | function download:setUri(val) 207 | self._uri=val 208 | end 209 | 210 | function download:getFilenamePrefix() 211 | return self._filenamePrefix 212 | end 213 | 214 | function download:setFilenamePrefix(val) 215 | self._filenamePrefix = val 216 | end 217 | 218 | function download:getExec() 219 | return self._exec 220 | end 221 | 222 | function download:setExec(val) 223 | self._exec=val 224 | end 225 | 226 | return download 227 | -------------------------------------------------------------------------------- /class/framework.lua: -------------------------------------------------------------------------------- 1 | local framework = {} 2 | 3 | function framework:download() 4 | local stable = self:getRelease():getStableVersion() 5 | stable:download() 6 | end 7 | 8 | -- LuaClassGen pregenerated functions 9 | 10 | function framework.new(init) 11 | init = init or {} 12 | 13 | local self={}--vapor.class.download.new(init) 14 | self.download=framework.download 15 | self._name=init.name 16 | self.getName=framework.getName 17 | self.setName=framework.setName 18 | self._image=init.image 19 | self.getImage=framework.getImage 20 | self.setImage=framework.setImage 21 | self._author=init.author 22 | self.getAuthor=framework.getAuthor 23 | self.setAuthor=framework.setAuthor 24 | self._website=init.website 25 | self.getWebsite=framework.getWebsite 26 | self.setWebsite=framework.setWebsite 27 | self._release=vapor.class.release.new(init.release) 28 | self.getRelease=framework.getRelease 29 | self.setRelease=framework.setRelease 30 | self._exec=init.exec 31 | self.getExec=framework.getExec 32 | self.setExec=framework.setExec 33 | self._execBasedir=init.execBasedir 34 | self.getExecBasedir=framework.getExecBasedir 35 | self.setExecBasedir=framework.setExecBasedir 36 | self._autoBinary=init.autoBinary 37 | self.getAutoBinary=framework.getAutoBinary 38 | self.setAutoBinary=framework.setAutoBinary 39 | self._identifier=init.identifier 40 | self.getIdentifier=framework.getIdentifier 41 | self.setIdentifier=framework.setIdentifier 42 | 43 | return self 44 | end 45 | 46 | function framework:getName() 47 | return self._name 48 | end 49 | 50 | function framework:setName(val) 51 | self._name=val 52 | end 53 | 54 | function framework:getImage() 55 | return self._image 56 | end 57 | 58 | function framework:setImage(val) 59 | self._image=val 60 | end 61 | 62 | function framework:getAuthor() 63 | return self._author 64 | end 65 | 66 | function framework:setAuthor(val) 67 | self._author=val 68 | end 69 | 70 | function framework:getWebsite() 71 | return self._website 72 | end 73 | 74 | function framework:setWebsite(val) 75 | self._website=val 76 | end 77 | 78 | function framework:getRelease() 79 | return self._release 80 | end 81 | 82 | function framework:setRelease(val) 83 | self._release=val 84 | end 85 | 86 | function framework:getExec() 87 | return self._exec 88 | end 89 | 90 | function framework:setExec(val) 91 | self._exec=val 92 | end 93 | 94 | function framework:getExecBasedir() 95 | return self._execBasedir 96 | end 97 | 98 | function framework:setExecBasedir(val) 99 | self._execBasedir=val 100 | end 101 | 102 | function framework:getAutoBinary() 103 | return self._autoBinary 104 | end 105 | 106 | function framework:setAutoBinary(val) 107 | self._autoBinary=val 108 | end 109 | 110 | function framework:getIdentifier() 111 | return self._identifier 112 | end 113 | 114 | function framework:setIdentifier(val) 115 | self._identifier=val 116 | end 117 | 118 | return framework 119 | -------------------------------------------------------------------------------- /class/game.lua: -------------------------------------------------------------------------------- 1 | local game = {} 2 | 3 | function game:play() 4 | local framework = vapor:getFrameworkObject(self:getFramework()) 5 | local framework_stable = framework:getRelease():getStableVersion() 6 | local binarypath = framework_stable:getPath().."/"..framework_stable:getExec() 7 | 8 | -- TODO: add local installed handlers 9 | if love._os == "Linux" then 10 | binarypath = "love" 11 | end 12 | 13 | local game_stable = self:getRelease():getStableVersion() 14 | local gamepath = game_stable:getFilename() 15 | 16 | local execstr 17 | if love._os == "Windows" then 18 | local fstr = [[start "" "%s" "%%APPDATA%%/LOVE/vapor1.x/%s"]] 19 | execstr = fstr:format(binarypath, gamepath) 20 | else 21 | -- OS X, Linux 22 | local fstr = [["%s" "%s/%s" &]] 23 | execstr = fstr:format(binarypath, love.filesystem.getSaveDirectory(), gamepath) 24 | end 25 | print(gamepath.." starting.") 26 | return os.execute(execstr) 27 | 28 | end 29 | 30 | function game:download() 31 | local stable = self:getRelease():getStableVersion() 32 | stable:download() 33 | end 34 | 35 | -- LuaClassGen pregenerated functions 36 | 37 | function game.new(init) 38 | init = init or {} 39 | local self={}--vapor.class.download.new(init) 40 | self.play=game.play 41 | self.download=game.download 42 | self._name=init.name 43 | self.getName=game.getName 44 | self.setName=game.setName 45 | self._framework=init.framework 46 | self.getFramework=game.getFramework 47 | self.setFramework=game.setFramework 48 | self._image=init.image 49 | self.getImage=game.getImage 50 | self.setImage=game.setImage 51 | self._description=init.description 52 | self.getDescription=game.getDescription 53 | self.setDescription=game.setDescription 54 | self._author=init.author 55 | self.getAuthor=game.getAuthor 56 | self.setAuthor=game.setAuthor 57 | self._website=init.website 58 | self.getWebsite=game.getWebsite 59 | self.setWebsite=game.setWebsite 60 | self._release=vapor.class.release.new(init.release) 61 | self.getRelease=game.getRelease 62 | self.setRelease=game.setRelease 63 | self._identifier=init.identifier 64 | self.getIdentifier=game.getIdentifier 65 | self.setIdentifier=game.setIdentifier 66 | return self 67 | end 68 | 69 | function game:getName() 70 | return self._name 71 | end 72 | 73 | function game:setName(val) 74 | self._name=val 75 | end 76 | 77 | function game:getFramework() 78 | return self._framework 79 | end 80 | 81 | function game:setFramework(val) 82 | self._framework=val 83 | end 84 | 85 | function game:getImage() 86 | return self._image 87 | end 88 | 89 | function game:setImage(val) 90 | self._image=val 91 | end 92 | 93 | function game:getDescription() 94 | return self._description 95 | end 96 | 97 | function game:setDescription(val) 98 | self._description=val 99 | end 100 | 101 | function game:getAuthor() 102 | return self._author 103 | end 104 | 105 | function game:setAuthor(val) 106 | self._author=val 107 | end 108 | 109 | function game:getWebsite() 110 | return self._website 111 | end 112 | 113 | function game:setWebsite(val) 114 | self._website=val 115 | end 116 | 117 | function game:getRelease() 118 | return self._release 119 | end 120 | 121 | function game:setRelease(val) 122 | self._release=val 123 | end 124 | 125 | function game:getIdentifier() 126 | return self._identifier 127 | end 128 | 129 | function game:setIdentifier(val) 130 | self._identifier=val 131 | end 132 | 133 | return game 134 | -------------------------------------------------------------------------------- /class/image.lua: -------------------------------------------------------------------------------- 1 | local image = {} 2 | 3 | -- LuaClassGen pregenerated functions 4 | 5 | function image.new(init) 6 | init = init or {} 7 | local self=vapor.class.download.new(init) 8 | self._release=init.release 9 | self.getRelease=image.getRelease 10 | self.setRelease=image.setRelease 11 | self._default=init.default 12 | self.getDefault=image.getDefault 13 | self.setDefault=image.setDefault 14 | return self 15 | end 16 | 17 | function image:getRelease() 18 | return self._release 19 | end 20 | 21 | function image:setRelease(val) 22 | self._release=val 23 | end 24 | 25 | function image:getDefault() 26 | return self._default 27 | end 28 | 29 | function image:setDefault(val) 30 | self._default=val 31 | end 32 | 33 | return image 34 | -------------------------------------------------------------------------------- /class/mirror.lua: -------------------------------------------------------------------------------- 1 | local mirror = {} 2 | 3 | -- LuaClassGen pregenerated functions 4 | 5 | function mirror.new(init) 6 | init = init or {} 7 | local self=vapor.class.download.new(init) 8 | 9 | -- TODO: make mirrors fs safe 10 | self:setFilenamePrefix("mirror") 11 | self:setIdentifier(init.uri) 12 | 13 | local download_update = self.update 14 | self.update = function() 15 | local ret = download_update(self) 16 | if self:getStatus() == vapor.status.downloaded then 17 | -- TODO: make async 18 | self:setStatus(vapor.status.processing) 19 | -- This is the basic validation function for 1.0 api. 20 | local success,data = pcall(function() 21 | 22 | local os = "win" 23 | 24 | local data = vapor.util.json.decode( self._data ) 25 | assert(data.api_version == "1.0","invalid api version") 26 | local r = {} 27 | 28 | r.games = {} 29 | for _,game in pairs(data.games) do 30 | local g = {} 31 | assert(game.id) 32 | g.identifier = tonumber(game.id) 33 | assert(game.name) 34 | g.name = game.name 35 | assert(game.framework) 36 | g.framework = game.framework 37 | assert(game.image) 38 | g.image = game.image 39 | g.description = game.description or "N/A" 40 | g.author = game.author or "N/A" 41 | g.website = game.website or "N/A" 42 | 43 | g.release = {} 44 | g.release.stable = game.release.stable 45 | g.release.versions = {} 46 | for _,version in pairs(game.release.versions) do 47 | local v = {} 48 | assert(version.id) 49 | v.identifier = tonumber(version.id) 50 | assert(version.uri) 51 | v.uri = version.uri 52 | assert(version.size) 53 | v.size = version.size 54 | assert(version.hash) 55 | v.hash = version.hash 56 | table.insert(g.release.versions,v) 57 | end 58 | table.insert(r.games,g) 59 | end 60 | 61 | r.frameworks = {} 62 | for _,framework in pairs(data.frameworks) do 63 | local f = {} 64 | assert(framework.id) 65 | f.identifier = tonumber(framework.id) 66 | assert(framework.name) 67 | f.name = framework.name 68 | assert(framework.image) 69 | f.image = framework.image 70 | f.author = framework.author or "N/A" 71 | f.website = framework.website or "N/A" 72 | table.insert(r.frameworks,f) 73 | 74 | f.release = {} 75 | f.release.stable = framework.release.stable 76 | f.release.versions = {} 77 | for _,version in pairs(framework.release.versions) do 78 | local v = {} 79 | assert(version.id) 80 | v.identifier = tonumber(version.id) 81 | assert(version[os.."-uri"]) 82 | v.uri = version[os.."-uri"] 83 | assert(version[os.."-size"]) 84 | v.size = version[os.."-size"] 85 | assert(version[os.."-hash"]) 86 | v.hash = version[os.."-hash"] 87 | v.unzip = version[os.."-unzip"] or false 88 | assert(version[os.."-exec"]) 89 | v.exec = version[os.."-exec"] 90 | table.insert(f.release.versions,v) 91 | end 92 | end 93 | 94 | return r 95 | end) 96 | 97 | if success then 98 | --vapor.util.print_r(data) 99 | self:setStatus(vapor.status.processed) 100 | self._processedData = data 101 | else 102 | print("error: "..data) 103 | self:setStatus(vapor.status.fail) 104 | end 105 | 106 | end 107 | return ret 108 | end 109 | 110 | return self 111 | end 112 | 113 | return mirror 114 | -------------------------------------------------------------------------------- /class/release.lua: -------------------------------------------------------------------------------- 1 | local release = {} 2 | 3 | function release:getVersionCollection() 4 | return self._versionCollection 5 | end 6 | 7 | function release:getStableVersion() 8 | local stable = self:getStable() 9 | local stable_versions = self._versionCollection:find( 10 | function(o) 11 | return o:getIdentifier() == stable 12 | end) 13 | return stable_versions[1] 14 | end 15 | 16 | -- LuaClassGen pregenerated functions 17 | 18 | function release.new(init) 19 | init = init or {} 20 | local self={} 21 | self._versionCollection = vapor.class.collection.new() 22 | self.getVersionCollection=release.getVersionCollection 23 | self.getStableVersion=release.getStableVersion 24 | self._stable=init.stable 25 | self.getStable=release.getStable 26 | self.setStable=release.setStable 27 | return self 28 | end 29 | 30 | function release:getStable() 31 | return self._stable 32 | end 33 | 34 | function release:setStable(val) 35 | self._stable=val 36 | end 37 | 38 | return release 39 | -------------------------------------------------------------------------------- /class/source.lua: -------------------------------------------------------------------------------- 1 | local source = {} 2 | 3 | -- LuaClassGen pregenerated functions 4 | 5 | function source.new(init) 6 | init = init or {} 7 | local self=vapor.class.download.new(init) 8 | self._playPingUri=init.playPingUri 9 | self.getPlayPingUri=source.getPlayPingUri 10 | self.setPlayPingUri=source.setPlayPingUri 11 | self._downloadPingUri=init.downloadPingUri 12 | self.getDownloadPingUri=source.getDownloadPingUri 13 | self.setDownloadPingUri=source.setDownloadPingUri 14 | return self 15 | end 16 | 17 | function source:getPlayPingUri() 18 | return self._playPingUri 19 | end 20 | 21 | function source:setPlayPingUri(val) 22 | self._playPingUri=val 23 | end 24 | 25 | function source:getDownloadPingUri() 26 | return self._downloadPingUri 27 | end 28 | 29 | function source:setDownloadPingUri(val) 30 | self._downloadPingUri=val 31 | end 32 | 33 | return source 34 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --- Vapor - LÖVE Distribution Client 2 | -- @module Vapor 3 | -- @author Josef N Patoprsty 4 | -- @copyright 2015 5 | -- @license zlib/libpng 6 | 7 | local vapor = { 8 | _VERSION = "Vapor 1.x", 9 | _DESCRIPTION = "LÖVE Distribution Client", 10 | _URL = "http://vapor.love2d.org", 11 | _LICENSE = [[ 12 | The zlib/libpng License 13 | Copyright (c) 2015 Josef N Patoprsty 14 | This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. 15 | Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 16 | 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 18 | 3. This notice may not be removed or altered from any source distribution. 19 | ]] 20 | } 21 | 22 | local cwd = (...):gsub('%.[^%.]+$', '') 23 | 24 | vapor.class = { 25 | game = require (cwd..".class.game"), 26 | framework = require (cwd..".class.framework"), 27 | image = require (cwd..".class.image"), 28 | release = require (cwd..".class.release"), 29 | mirror = require (cwd..".class.mirror"), 30 | source = require (cwd..".class.source"), 31 | download = require (cwd..".class.download"), 32 | collection = require (cwd..".class.collection"), 33 | } 34 | 35 | vapor.util = require (cwd..".util") 36 | vapor.status = { 37 | fail = -1, 38 | uninitialized = 0, 39 | ready = 1, 40 | download = 2, 41 | downloading = 3, 42 | downloaded = 4, 43 | hashing = 5, 44 | processing = 6, 45 | processed = 7, 46 | } 47 | vapor.lang = require (cwd..".lang") 48 | 49 | function vapor:getGameCollection() 50 | return self._gameCollection 51 | end 52 | 53 | function vapor:getGameObject(id) 54 | assert(type(id) == "number") 55 | local games = self._gameCollection:find( 56 | function(g) 57 | return g:getIdentifier() == id 58 | end) 59 | return games[1] 60 | end 61 | 62 | function vapor:getFrameworkCollection() 63 | return self._frameworkCollection 64 | end 65 | 66 | function vapor:getFrameworkObject(id) 67 | assert(type(id) == "number") 68 | local frameworks = self._frameworkCollection:find( 69 | function(f) 70 | return f:getIdentifier() == id 71 | end) 72 | return frameworks[1] 73 | end 74 | 75 | function vapor:getMirrorCollection() 76 | return self._mirrorCollection 77 | end 78 | 79 | function vapor:getSourceCollection() 80 | return self._sourceCollection 81 | end 82 | 83 | function vapor:update() 84 | for _,v in pairs(self._gameCollection:all()) do 85 | --v:update() 86 | for _,w in pairs(v:getRelease():getVersionCollection():all()) do 87 | w:update() 88 | end 89 | end 90 | for _,v in pairs(self._frameworkCollection:all()) do 91 | --v:update() 92 | for _,w in pairs(v:getRelease():getVersionCollection():all()) do 93 | w:update() 94 | end 95 | end 96 | for _,v in pairs(self._mirrorCollection:all()) do 97 | v:update() 98 | if v:getStatus() == vapor.status.processed then 99 | for _,game in pairs(v._processedData.games) do 100 | local g = self._gameCollection:add( vapor.class.game.new(game) ) 101 | for _,version in pairs(game.release.versions) do 102 | local v = vapor.class.download.new(version) 103 | v:setFilenamePrefix('game_'..g:getIdentifier()) 104 | g:getRelease():getVersionCollection():add( v ) 105 | end 106 | end 107 | for _,framework in pairs(v._processedData.frameworks) do 108 | local f = self._frameworkCollection:add( vapor.class.framework.new(framework) ) 109 | for _,version in pairs(framework.release.versions) do 110 | local v = vapor.class.download.new(version) 111 | v:setFilenamePrefix('framework_'..f:getIdentifier()) 112 | f:getRelease():getVersionCollection():add( v ) 113 | end 114 | end 115 | v:setStatus(vapor.status.ready) 116 | end 117 | end 118 | for _,v in pairs(self._sourceCollection:all()) do 119 | v:update() 120 | end 121 | end 122 | 123 | -- LuaClassGen pregenerated functions 124 | 125 | function vapor.new(init) 126 | init = init or {} 127 | local self={} 128 | 129 | self._gameCollection = vapor.class.collection.new() 130 | self.getGameCollection=vapor.getGameCollection 131 | self.getGameObject=vapor.getGameObject 132 | 133 | self._frameworkCollection = vapor.class.collection.new() 134 | self.getFrameworkCollection=vapor.getFrameworkCollection 135 | self.getFrameworkObject=vapor.getFrameworkObject 136 | 137 | self._mirrorCollection = vapor.class.collection.new() 138 | self.getMirrorCollection=vapor.getMirrorCollection 139 | 140 | self._sourceCollection = vapor.class.collection.new() 141 | self.getSourceCollection=vapor.getSourceCollection 142 | 143 | self._cacheDir=init.cacheDir 144 | self.getCacheDir=vapor.getCacheDir 145 | self.setCacheDir=vapor.setCacheDir 146 | 147 | self.update = vapor.update 148 | 149 | self.class=vapor.class 150 | self.util = vapor.util 151 | self.status = vapor.status 152 | self.labg = vapor.lang 153 | 154 | return self 155 | end 156 | 157 | function vapor:getCacheDir() 158 | return self._cacheDir 159 | end 160 | 161 | function vapor:setCacheDir(val) 162 | self._cacheDir=val 163 | end 164 | 165 | return vapor 166 | -------------------------------------------------------------------------------- /json.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- JSON4Lua: JSON encoding / decoding support for the Lua language. 3 | -- json Module. 4 | -- Author: Craig Mason-Jones 5 | -- Homepage: http://github.com/craigmj/json4lua/ 6 | -- Version: 1.0.0 7 | -- This module is released under the MIT License (MIT). 8 | -- Please see LICENCE.txt for details. 9 | -- 10 | -- USAGE: 11 | -- This module exposes two functions: 12 | -- json.encode(o) 13 | -- Returns the table / string / boolean / number / nil / json.null value as a JSON-encoded string. 14 | -- json.decode(json_string) 15 | -- Returns a Lua object populated with the data encoded in the JSON string json_string. 16 | -- 17 | -- REQUIREMENTS: 18 | -- compat-5.1 if using Lua 5.0 19 | -- 20 | -- CHANGELOG 21 | -- 0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix). 22 | -- Fixed Lua 5.1 compatibility issues. 23 | -- Introduced json.null to have null values in associative arrays. 24 | -- json.encode() performance improvement (more than 50%) through table.concat rather than .. 25 | -- Introduced decode ability to ignore /**/ comments in the JSON string. 26 | -- 0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays. 27 | ----------------------------------------------------------------------------- 28 | 29 | ----------------------------------------------------------------------------- 30 | -- Imports and dependencies 31 | ----------------------------------------------------------------------------- 32 | local math = require('math') 33 | local string = require("string") 34 | local table = require("table") 35 | 36 | ----------------------------------------------------------------------------- 37 | -- Module declaration 38 | ----------------------------------------------------------------------------- 39 | local json = {} -- Public namespace 40 | local json_private = {} -- Private namespace 41 | 42 | -- Public functions 43 | 44 | -- Private functions 45 | local decode_scanArray 46 | local decode_scanComment 47 | local decode_scanConstant 48 | local decode_scanNumber 49 | local decode_scanObject 50 | local decode_scanString 51 | local decode_scanWhitespace 52 | local encodeString 53 | local isArray 54 | local isEncodable 55 | 56 | ----------------------------------------------------------------------------- 57 | -- PUBLIC FUNCTIONS 58 | ----------------------------------------------------------------------------- 59 | --- Encodes an arbitrary Lua object / variable. 60 | -- @param v The Lua object / variable to be JSON encoded. 61 | -- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode) 62 | function json.encode (v) 63 | -- Handle nil values 64 | if v==nil then 65 | return "null" 66 | end 67 | 68 | local vtype = type(v) 69 | 70 | -- Handle strings 71 | if vtype=='string' then 72 | return '"' .. json_private.encodeString(v) .. '"' -- Need to handle encoding in string 73 | end 74 | 75 | -- Handle booleans 76 | if vtype=='number' or vtype=='boolean' then 77 | return tostring(v) 78 | end 79 | 80 | -- Handle tables 81 | if vtype=='table' then 82 | local rval = {} 83 | -- Consider arrays separately 84 | local bArray, maxCount = isArray(v) 85 | if bArray then 86 | for i = 1,maxCount do 87 | table.insert(rval, json.encode(v[i])) 88 | end 89 | else -- An object, not an array 90 | for i,j in pairs(v) do 91 | if isEncodable(i) and isEncodable(j) then 92 | table.insert(rval, '"' .. json_private.encodeString(i) .. '":' .. json.encode(j)) 93 | end 94 | end 95 | end 96 | if bArray then 97 | return '[' .. table.concat(rval,',') ..']' 98 | else 99 | return '{' .. table.concat(rval,',') .. '}' 100 | end 101 | end 102 | 103 | -- Handle null values 104 | if vtype=='function' and v==null then 105 | return 'null' 106 | end 107 | 108 | assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. tostring(v)) 109 | end 110 | 111 | 112 | --- Decodes a JSON string and returns the decoded value as a Lua data structure / value. 113 | -- @param s The string to scan. 114 | -- @param [startPos] Optional starting position where the JSON string is located. Defaults to 1. 115 | -- @param Lua object, number The object that was scanned, as a Lua table / string / number / boolean or nil, 116 | -- and the position of the first character after 117 | -- the scanned JSON object. 118 | function json.decode(s, startPos) 119 | startPos = startPos and startPos or 1 120 | startPos = decode_scanWhitespace(s,startPos) 121 | assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']') 122 | local curChar = string.sub(s,startPos,startPos) 123 | -- Object 124 | if curChar=='{' then 125 | return decode_scanObject(s,startPos) 126 | end 127 | -- Array 128 | if curChar=='[' then 129 | return decode_scanArray(s,startPos) 130 | end 131 | -- Number 132 | if string.find("+-0123456789.e", curChar, 1, true) then 133 | return decode_scanNumber(s,startPos) 134 | end 135 | -- String 136 | if curChar==[["]] or curChar==[[']] then 137 | return decode_scanString(s,startPos) 138 | end 139 | if string.sub(s,startPos,startPos+1)=='/*' then 140 | return decode(s, decode_scanComment(s,startPos)) 141 | end 142 | -- Otherwise, it must be a constant 143 | return decode_scanConstant(s,startPos) 144 | end 145 | 146 | --- The null function allows one to specify a null value in an associative array (which is otherwise 147 | -- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null } 148 | function null() 149 | return null -- so json.null() will also return null ;-) 150 | end 151 | ----------------------------------------------------------------------------- 152 | -- Internal, PRIVATE functions. 153 | -- Following a Python-like convention, I have prefixed all these 'PRIVATE' 154 | -- functions with an underscore. 155 | ----------------------------------------------------------------------------- 156 | 157 | --- Scans an array from JSON into a Lua object 158 | -- startPos begins at the start of the array. 159 | -- Returns the array and the next starting position 160 | -- @param s The string being scanned. 161 | -- @param startPos The starting position for the scan. 162 | -- @return table, int The scanned array as a table, and the position of the next character to scan. 163 | function decode_scanArray(s,startPos) 164 | local array = {} -- The return value 165 | local stringLen = string.len(s) 166 | assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s ) 167 | startPos = startPos + 1 168 | -- Infinite loop for array elements 169 | repeat 170 | startPos = decode_scanWhitespace(s,startPos) 171 | assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.') 172 | local curChar = string.sub(s,startPos,startPos) 173 | if (curChar==']') then 174 | return array, startPos+1 175 | end 176 | if (curChar==',') then 177 | startPos = decode_scanWhitespace(s,startPos+1) 178 | end 179 | assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.') 180 | object, startPos = json.decode(s,startPos) 181 | table.insert(array,object) 182 | until false 183 | end 184 | 185 | --- Scans a comment and discards the comment. 186 | -- Returns the position of the next character following the comment. 187 | -- @param string s The JSON string to scan. 188 | -- @param int startPos The starting position of the comment 189 | function decode_scanComment(s, startPos) 190 | assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos) 191 | local endPos = string.find(s,'*/',startPos+2) 192 | assert(endPos~=nil, "Unterminated comment in string at " .. startPos) 193 | return endPos+2 194 | end 195 | 196 | --- Scans for given constants: true, false or null 197 | -- Returns the appropriate Lua type, and the position of the next character to read. 198 | -- @param s The string being scanned. 199 | -- @param startPos The position in the string at which to start scanning. 200 | -- @return object, int The object (true, false or nil) and the position at which the next character should be 201 | -- scanned. 202 | function decode_scanConstant(s, startPos) 203 | local consts = { ["true"] = true, ["false"] = false, ["null"] = nil } 204 | local constNames = {"true","false","null"} 205 | 206 | for i,k in pairs(constNames) do 207 | if string.sub(s,startPos, startPos + string.len(k) -1 )==k then 208 | return consts[k], startPos + string.len(k) 209 | end 210 | end 211 | assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos) 212 | end 213 | 214 | --- Scans a number from the JSON encoded string. 215 | -- (in fact, also is able to scan numeric +- eqns, which is not 216 | -- in the JSON spec.) 217 | -- Returns the number, and the position of the next character 218 | -- after the number. 219 | -- @param s The string being scanned. 220 | -- @param startPos The position at which to start scanning. 221 | -- @return number, int The extracted number and the position of the next character to scan. 222 | function decode_scanNumber(s,startPos) 223 | local endPos = startPos+1 224 | local stringLen = string.len(s) 225 | local acceptableChars = "+-0123456789.e" 226 | while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true) 227 | and endPos<=stringLen 228 | ) do 229 | endPos = endPos + 1 230 | end 231 | local stringValue = 'return ' .. string.sub(s,startPos, endPos-1) 232 | local stringEval = loadstring(stringValue) 233 | assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos) 234 | return stringEval(), endPos 235 | end 236 | 237 | --- Scans a JSON object into a Lua object. 238 | -- startPos begins at the start of the object. 239 | -- Returns the object and the next starting position. 240 | -- @param s The string being scanned. 241 | -- @param startPos The starting position of the scan. 242 | -- @return table, int The scanned object as a table and the position of the next character to scan. 243 | function decode_scanObject(s,startPos) 244 | local object = {} 245 | local stringLen = string.len(s) 246 | local key, value 247 | assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s) 248 | startPos = startPos + 1 249 | repeat 250 | startPos = decode_scanWhitespace(s,startPos) 251 | assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.') 252 | local curChar = string.sub(s,startPos,startPos) 253 | if (curChar=='}') then 254 | return object,startPos+1 255 | end 256 | if (curChar==',') then 257 | startPos = decode_scanWhitespace(s,startPos+1) 258 | end 259 | assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.') 260 | -- Scan the key 261 | key, startPos = json.decode(s,startPos) 262 | assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) 263 | startPos = decode_scanWhitespace(s,startPos) 264 | assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) 265 | assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos) 266 | startPos = decode_scanWhitespace(s,startPos+1) 267 | assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) 268 | value, startPos = json.decode(s,startPos) 269 | object[key]=value 270 | until false -- infinite loop while key-value pairs are found 271 | end 272 | 273 | -- START SoniEx2 274 | -- Initialize some things used by decode_scanString 275 | -- You know, for efficiency 276 | local escapeSequences = { 277 | ["\\t"] = "\t", 278 | ["\\f"] = "\f", 279 | ["\\r"] = "\r", 280 | ["\\n"] = "\n", 281 | ["\\b"] = "\b" 282 | } 283 | setmetatable(escapeSequences, {__index = function(t,k) 284 | -- skip "\" aka strip escape 285 | return string.sub(k,2) 286 | end}) 287 | -- END SoniEx2 288 | 289 | --- Scans a JSON string from the opening inverted comma or single quote to the 290 | -- end of the string. 291 | -- Returns the string extracted as a Lua string, 292 | -- and the position of the next non-string character 293 | -- (after the closing inverted comma or single quote). 294 | -- @param s The string being scanned. 295 | -- @param startPos The starting position of the scan. 296 | -- @return string, int The extracted string as a Lua string, and the next character to parse. 297 | function decode_scanString(s,startPos) 298 | assert(startPos, 'decode_scanString(..) called without start position') 299 | local startChar = string.sub(s,startPos,startPos) 300 | -- START SoniEx2 301 | -- PS: I don't think single quotes are valid JSON 302 | assert(startChar == [["]] or startChar == [[']],'decode_scanString called for a non-string') 303 | --assert(startPos, "String decoding failed: missing closing " .. startChar .. " for string at position " .. oldStart) 304 | local t = {} 305 | local i,j = startPos,startPos 306 | while string.find(s, startChar, j+1) ~= j+1 do 307 | local oldj = j 308 | i,j = string.find(s, "\\.", j+1) 309 | local x,y = string.find(s, startChar, oldj+1) 310 | if not i or x < i then 311 | i,j = x,y-1 312 | end 313 | table.insert(t, string.sub(s, oldj+1, i-1)) 314 | if string.sub(s, i, j) == "\\u" then 315 | local a = string.sub(s,j+1,j+4) 316 | j = j + 4 317 | local n = tonumber(a, 16) 318 | assert(n, "String decoding failed: bad Unicode escape " .. a .. " at position " .. i .. " : " .. j) 319 | -- math.floor(x/2^y) == lazy right shift 320 | -- a % 2^b == bitwise_and(a, (2^b)-1) 321 | -- 64 = 2^6 322 | -- 4096 = 2^12 (or 2^6 * 2^6) 323 | local x 324 | if n < 0x80 then 325 | x = string.char(n % 0x80) 326 | elseif n < 0x800 then 327 | -- [110x xxxx] [10xx xxxx] 328 | x = string.char(0xC0 + (math.floor(n/64) % 0x20), 0x80 + (n % 0x40)) 329 | else 330 | -- [1110 xxxx] [10xx xxxx] [10xx xxxx] 331 | x = string.char(0xE0 + (math.floor(n/4096) % 0x10), 0x80 + (math.floor(n/64) % 0x40), 0x80 + (n % 0x40)) 332 | end 333 | table.insert(t, x) 334 | else 335 | table.insert(t, escapeSequences[string.sub(s, i, j)]) 336 | end 337 | end 338 | table.insert(t,string.sub(j, j+1)) 339 | assert(string.find(s, startChar, j+1), "String decoding failed: missing closing " .. startChar .. " at position " .. j .. "(for string at position " .. startPos .. ")") 340 | return table.concat(t,""), j+2 341 | -- END SoniEx2 342 | end 343 | 344 | --- Scans a JSON string skipping all whitespace from the current start position. 345 | -- Returns the position of the first non-whitespace character, or nil if the whole end of string is reached. 346 | -- @param s The string being scanned 347 | -- @param startPos The starting position where we should begin removing whitespace. 348 | -- @return int The first position where non-whitespace was encountered, or string.len(s)+1 if the end of string 349 | -- was reached. 350 | function decode_scanWhitespace(s,startPos) 351 | local whitespace=" \n\r\t" 352 | local stringLen = string.len(s) 353 | while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true) and startPos <= stringLen) do 354 | startPos = startPos + 1 355 | end 356 | return startPos 357 | end 358 | 359 | --- Encodes a string to be JSON-compatible. 360 | -- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-) 361 | -- @param s The string to return as a JSON encoded (i.e. backquoted string) 362 | -- @return The string appropriately escaped. 363 | 364 | local escapeList = { 365 | ['"'] = '\\"', 366 | ['\\'] = '\\\\', 367 | ['/'] = '\\/', 368 | ['\b'] = '\\b', 369 | ['\f'] = '\\f', 370 | ['\n'] = '\\n', 371 | ['\r'] = '\\r', 372 | ['\t'] = '\\t' 373 | } 374 | 375 | function json_private.encodeString(s) 376 | local s = tostring(s) 377 | return s:gsub(".", function(c) return escapeList[c] end) -- SoniEx2: 5.0 compat 378 | end 379 | 380 | -- Determines whether the given Lua type is an array or a table / dictionary. 381 | -- We consider any table an array if it has indexes 1..n for its n items, and no 382 | -- other data in the table. 383 | -- I think this method is currently a little 'flaky', but can't think of a good way around it yet... 384 | -- @param t The table to evaluate as an array 385 | -- @return boolean, number True if the table can be represented as an array, false otherwise. If true, 386 | -- the second returned value is the maximum 387 | -- number of indexed elements in the array. 388 | function isArray(t) 389 | -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable 390 | -- (with the possible exception of 'n') 391 | local maxIndex = 0 392 | for k,v in pairs(t) do 393 | if (type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair 394 | if (not isEncodable(v)) then return false end -- All array elements must be encodable 395 | maxIndex = math.max(maxIndex,k) 396 | else 397 | if (k=='n') then 398 | if v ~= table.getn(t) then return false end -- False if n does not hold the number of elements 399 | else -- Else of (k=='n') 400 | if isEncodable(v) then return false end 401 | end -- End of (k~='n') 402 | end -- End of k,v not an indexed pair 403 | end -- End of loop across all pairs 404 | return true, maxIndex 405 | end 406 | 407 | --- Determines whether the given Lua object / table / variable can be JSON encoded. The only 408 | -- types that are JSON encodable are: string, boolean, number, nil, table and json.null. 409 | -- In this implementation, all other types are ignored. 410 | -- @param o The object to examine. 411 | -- @return boolean True if the object should be JSON encoded, false if it should be ignored. 412 | function isEncodable(o) 413 | local t = type(o) 414 | return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) 415 | end 416 | 417 | return json -------------------------------------------------------------------------------- /lang.lua: -------------------------------------------------------------------------------- 1 | local lang = {} 2 | 3 | function lang.translate() 4 | end 5 | 6 | return lang 7 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The zlib/libpng License 2 | Copyright (c) 2015 Josef Patoprsty 3 | 4 | This software is provided 'as-is', without any express or implied warranty. In 5 | no event will the authors be held liable for any damages arising from the use 6 | of this software. 7 | 8 | Permission is granted to anyone to use this software for any purpose, including 9 | commercial applications, and to alter it and redistribute it freely, subject to 10 | the following restrictions: 11 | 12 | 1. The origin of this software must not be misrepresented; you must not claim 13 | that you wrote the original software. If you use this software in a product, an 14 | acknowledgment in the product documentation would be appreciated but is not 15 | required. 16 | 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 20 | 3. This notice may not be removed or altered from any source distribution. 21 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #Vapor - LÖVE Distribution Client 2 | 3 | ![Screenshot](https://github.com/josefnpat/vapor/blob/vapor0.x/dev/vaporlogo/vapT800x300.png?raw=true) 4 | 5 | This branch is for the 1.x release of Vapor, currently under development. 6 | 7 | _For the old version, please visit the [0.x branch](https://github.com/josefnpat/vapor/tree/vapor0.x)._ 8 | 9 | Visit the official website at [vapor.love2d.org](http://vapor.love2d.org) 10 | -------------------------------------------------------------------------------- /util.lua: -------------------------------------------------------------------------------- 1 | local util = {} 2 | 3 | local cwd = (...):gsub('%.[^%.]+$', '') 4 | 5 | util.http = require "socket.http" 6 | util.json = require(cwd..".json") 7 | 8 | -- from https://gist.github.com/nrk/31175 9 | util.print_r = function ( t ) 10 | local print_r_cache={} 11 | local function sub_print_r(t,indent) 12 | if (print_r_cache[tostring(t)]) then 13 | print(indent.."*"..tostring(t)) 14 | else 15 | print_r_cache[tostring(t)]=true 16 | if (type(t)=="table") then 17 | for pos,val in pairs(t) do 18 | if (type(val)=="table") then 19 | print(indent.."["..pos.."] => "..tostring(t).." {") 20 | sub_print_r(val,indent..string.rep(" ",string.len(pos)+8)) 21 | print(indent..string.rep(" ",string.len(pos)+6).."}") 22 | else 23 | print(indent.."["..pos.."] => "..tostring(val)) 24 | end 25 | end 26 | else 27 | print(indent..tostring(t)) 28 | end 29 | end 30 | end 31 | sub_print_r(t," ") 32 | end 33 | 34 | -- modified from bobbyjones' unZipLove from addCompat 35 | util.unzip = function ( fn ) 36 | love.filesystem.mount( fn, string.gsub(fn, "%.([%w%-]+)$", "") ) 37 | love.filesystem.createDirectory( "unzipped-"..string.gsub(fn, "%.([%w%-]+)$", "") ) 38 | local path = string.gsub(fn, "%.([%w%-]+)$", "") 39 | local function callback(folder) 40 | --print('folder:',folder) 41 | end 42 | local folder = love.filesystem.getDirectoryItems( 43 | string.gsub(fn, "%.([%w%-]+)$", ""), callback ) 44 | local function recurseFolder( folder, path, prefix ) 45 | for i,file in ipairs( folder ) do 46 | if love.filesystem.isDirectory( path .. "/".. file ) then 47 | local folder = love.filesystem.getDirectoryItems(path.."/"..file,callback) 48 | love.filesystem.createDirectory(prefix..path.."/"..file) 49 | recurseFolder( folder, path .. "/".. file, prefix ) 50 | elseif love.filesystem.isFile( path .."/" .. file ) then 51 | local content = love.filesystem.read(path.."/"..file) 52 | love.filesystem.write(prefix..path.."/"..file, content) 53 | end 54 | end 55 | return prefix..path 56 | end 57 | return recurseFolder( folder, path, "unzipped-") 58 | end 59 | 60 | return util 61 | --------------------------------------------------------------------------------