79 |
80 |
81 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | 1 ) {
36 | $forwarded[strtoupper( $kv[0] )] = $kv[1];
37 | }
38 | }
39 |
40 | if( array_key_exists( 'HOST', $forwarded ) ) {
41 | $_SERVER['HTTP_HOST'] = $forwarded['HOST'];
42 | }
43 |
44 | if( array_key_exists( 'PROTO', $forwarded ) && strtoupper( $forwarded['PROTO'] ) === 'HTTPS' ) {
45 | $_SERVER['HTTPS'] = 'on';
46 | }
47 | } else {
48 | if( isset( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) {
49 | $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
50 | }
51 |
52 | if( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && strtoupper( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) === 'HTTPS' ) {
53 | $_SERVER['HTTPS'] = 'on';
54 | }
55 | }
56 |
57 | if( isset( $_SERVER['HTTPS'] ) )
58 | $protocol = 'https://';
59 | else
60 | $protocol = 'http://';
61 |
62 | if( isset( $_ENV['LINEAGEOTA_BASE_PATH'] ) )
63 | $base_path = $_ENV['LINEAGEOTA_BASE_PATH'];
64 | else
65 | $base_path = $protocol.$_SERVER['HTTP_HOST'].dirname( $_SERVER['SCRIPT_NAME'] );
66 |
67 | $app = new CmOta();
68 | $app
69 | ->setConfig( 'basePath', $base_path )
70 | ->loadConfigJSON( 'lineageota.json' )
71 | ->setConfigJSON( 'githubRepos', 'github.json' )
72 | ->run();
73 |
--------------------------------------------------------------------------------
/src/Helpers/CurlRequest.php:
--------------------------------------------------------------------------------
1 | url = $url;
40 | $this->addHeader( 'user-agent: curl/7.68.0' ); // Make sure a user-agent is being sent
41 | }
42 |
43 | /**
44 | * Return the status code of the request
45 | * @param string $header The additional header to be sent
46 | */
47 | public function addHeader( $header ) {
48 | array_push( $this->header, $header );
49 | }
50 |
51 | /**
52 | * Executes the request and returns it's success
53 | * @return bool The success of the request
54 | */
55 | public function executeRequest() {
56 | $request = curl_init( $this->url );
57 |
58 | curl_setopt( $request, CURLOPT_RETURNTRANSFER, true );
59 | curl_setopt( $request, CURLOPT_HTTPHEADER, $this->header );
60 |
61 | $this->response = curl_exec( $request );
62 | $this->status = curl_getinfo( $request, CURLINFO_RESPONSE_CODE );
63 | curl_close( $request );
64 |
65 | if( $this->status == 200 ) return true;
66 |
67 | return false;
68 | }
69 |
70 | /* Getters */
71 |
72 | /**
73 | * Return the status code of the request
74 | * @return int The status code
75 | */
76 | public function getStatus() {
77 | return $this->status;
78 | }
79 |
80 | /**
81 | * Return the response of the request
82 | * @return string The response
83 | */
84 | public function getResponse() {
85 | return $this->response;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Helpers/BuildGithub.php:
--------------------------------------------------------------------------------
1 | importData( $data );
49 | } else {
50 | // Split all Assets because they are not properly sorted
51 | foreach( $release['assets'] as $asset ) {
52 | switch( $asset['content_type'] ) {
53 | case 'application/zip':
54 | array_push( $archives, $asset );
55 |
56 | break;
57 | default:
58 | $extension = pathinfo( $asset['name'], PATHINFO_EXTENSION );
59 |
60 | switch( $extension ) {
61 | case 'txt':
62 | case 'html':
63 | array_push( $changelogs, $asset );
64 |
65 | break;
66 | case 'md5sum':
67 | array_push( $md5sums, $asset );
68 |
69 | break;
70 | case 'prop':
71 | array_push( $properties, $asset );
72 |
73 | break;
74 | }
75 | }
76 | }
77 |
78 | // If there are multiple zip's in the release, grab the largest one.
79 | $largestSize = -1;
80 |
81 | foreach( $archives as $archive ) {
82 | if( $archive['size'] > $largestSize ) {
83 | $tokens = $this->parseFilenameFull($archive['name']);
84 |
85 | $this->filePath = $archive['browser_download_url'];
86 | $this->url = $archive['browser_download_url'];
87 | $this->channel = $this->_getChannel( str_replace( range( 0 , 9 ), '', $tokens['channel'] ), $tokens['type'], $tokens['version'] );
88 | $this->filename = $archive['name'];
89 | $this->timestamp = strtotime( $archive['updated_at'] );
90 | $this->model = $tokens['model'];
91 | $this->version = $tokens['version'];
92 | $this->size = $archive['size'];
93 |
94 | $largestSize = $this->size;
95 | }
96 | }
97 |
98 | foreach( $properties as $property ) {
99 | $this->buildProp = explode( "\n", file_get_contents( $property['browser_download_url'] ) );
100 | $this->timestamp = intval( $this->getBuildPropValue( 'ro.build.date.utc' ) ?? $this->timestamp );
101 | $this->incremental = $this->getBuildPropValue( 'ro.build.version.incremental' ) ?? '';
102 | $this->apiLevel = $this->getBuildPropValue( 'ro.build.version.sdk' ) ?? '';
103 | $this->model = $this->getBuildPropValue( 'ro.lineage.device' ) ?? $this->getBuildPropValue( 'ro.cm.device' ) ?? $this->model;
104 | }
105 |
106 | foreach ( $md5sums as $md5sum ) {
107 | $md5 = $this->parseMD5( $md5sum['browser_download_url'] );
108 |
109 | if( array_key_exists( $this->filename, $md5 ) ) {
110 | $this->md5 = $md5[$this->filename];
111 | }
112 | }
113 | foreach( $changelogs as $changelog ) {
114 | $this->changelogUrl = $changelog['browser_download_url'];
115 | }
116 |
117 | $this->uid = hash( 'sha256', $this->timestamp . $this->model . $this->apiLevel, false );
118 | }
119 | }
120 |
121 | /**
122 | * Create a delta build based from the current build to the target build.
123 | * @param type $targetToken The target build from where to build the Delta
124 | * @return array/boolean Return an array performatted with the correct data inside, otherwise false if not possible to be created
125 | */
126 | public function getDelta( $targetToken ){
127 | $ret = false;
128 |
129 | // TO-DO: Figuring out a way to provide a delta build over github
130 |
131 | return $ret;
132 | }
133 |
134 | /* Utility / Internal */
135 |
136 | /**
137 | * Return the MD5 value of the current build
138 | * @param string $file The path of the file containing the hashes
139 | * @return array The MD5 hashes
140 | */
141 | private function parseMD5( $file ){
142 | $ret = array( );
143 |
144 | $md5sums = explode( "\n", file_get_contents( $file ) );
145 |
146 | foreach( $md5sums as $md5sum ) {
147 | $md5 = explode( " ", $md5sum );
148 |
149 | if( count( $md5 ) == 2 ) {
150 | $ret[$md5[1]] = $md5[0];
151 | }
152 | }
153 |
154 | return $ret;
155 | }
156 |
157 | }
158 |
--------------------------------------------------------------------------------
/src/Helpers/BuildLocal.php:
--------------------------------------------------------------------------------
1 | importData( $data );
44 | } else {
45 | $tokens = $this->parseFilenameFull( $fileName );
46 |
47 | $this->filePath = $physicalPath . '/' . $fileName;
48 | $this->filename = $fileName;
49 |
50 | // Try to load the build.prop from two possible paths:
51 | // - builds/CURRENT_ZIP_FILE.zip/system/build.prop
52 | // - builds/CURRENT_ZIP_FILE.zip.prop ( which must exist )
53 | $propsFileContent = @file_get_contents( 'zip://' . $this->filePath . '#system/build.prop' );
54 |
55 | if( $propsFileContent === false || empty( $propsFileContent ) ) {
56 | $propsFileContent = @file_get_contents( $this->filePath . '.prop' );
57 | }
58 |
59 | $this->buildProp = explode( "\n", $propsFileContent );
60 |
61 | if ( $tokens['date'] == '' ) {
62 | $timestamp = filemtime( $this->filePath );
63 | } else {
64 | $timezone = date_default_timezone_get();
65 | date_default_timezone_set('Pacific/Kiritimati'); // the earliest time zone on Earth UTC+14:00
66 | $d = date_parse_from_format('Ymd', $tokens['date']);
67 | $timestamp = mktime(0, 0, 0, $d['month'], $d['day'], $d['year']);
68 | date_default_timezone_set($timezone);
69 | }
70 |
71 | // Try to fetch build.prop values. In some cases, we can provide a fallback, in other a null value will be given
72 | $this->channel = $this->_getChannel( $this->getBuildPropValue( 'ro.lineage.releasetype' ) ?? str_replace( range( 0 , 9 ), '', $tokens['channel'] ), $tokens['type'], $tokens['version'] );
73 | $this->timestamp = intval( $this->getBuildPropValue( 'ro.build.date.utc' ) ?? $timestamp );
74 | $this->incremental = $this->getBuildPropValue( 'ro.build.version.incremental' ) ?? '';
75 | $this->apiLevel = $this->getBuildPropValue( 'ro.build.version.sdk' ) ?? '';
76 | $this->model = $this->getBuildPropValue( 'ro.lineage.device' ) ?? $this->getBuildPropValue( 'ro.cm.device' ) ?? $tokens['model'];
77 | $this->version = $tokens['version'];
78 | $this->uid = hash( 'sha256', $this->timestamp . $this->model . $this->apiLevel, false );
79 | $this->size = filesize( $this->filePath );
80 |
81 | $position = strrpos( $physicalPath, '/builds/full' );
82 |
83 | if( $position === FALSE )
84 | $this->url = $this->_getUrl( '', Flight::cfg()->get( 'buildsPath' ) );
85 | else
86 | $this->url = $this->_getUrl( '', Flight::cfg()->get( 'basePath' ) . substr( $physicalPath, $position ) );
87 |
88 | $this->changelogUrl = $this->_getChangelogUrl();
89 | $this->md5 = $this->_getMD5();
90 | }
91 | }
92 |
93 | /**
94 | * Create a delta build based from the current build to the target build.
95 | * @param type $targetToken The target build from where to build the Delta
96 | * @return array/boolean Return an array performatted with the correct data inside, otherwise false if not possible to be created
97 | */
98 | public function getDelta( $targetToken ) {
99 | $ret = false;
100 |
101 | $deltaFile = $this->incremental . '-' . $targetToken->incremental . '.zip';
102 | $deltaFilePath = Flight::cfg()->get('realBasePath') . '/builds/delta/' . $deltaFile;
103 |
104 | if( file_exists( $deltaFilePath ) )
105 | $ret = array(
106 | 'filename' => $deltaFile,
107 | 'timestamp' => filemtime( $deltaFilePath ),
108 | 'md5' => $this->_getMD5( $deltaFilePath ),
109 | 'url' => $this->_getUrl( $deltaFile, Flight::cfg()->get( 'deltasPath' ) ),
110 | 'api_level' => $this->apiLevel,
111 | 'incremental' => $targetToken->incremental
112 | );
113 |
114 | return $ret;
115 | }
116 |
117 | /* Utility / Internal */
118 |
119 | /**
120 | * Return the MD5 value of the current build
121 | * @param string $path The path of the file
122 | * @return string The MD5 hash
123 | */
124 | private function _getMD5( $path = '' ) {
125 | $ret = '';
126 |
127 | if( empty($path) ) $path = $this->filePath;
128 |
129 | // Pretty much faster if it is available
130 | if( file_exists( $path . '.md5sum' ) ) {
131 | $tmp = explode( ' ', file_get_contents( $path . '.md5sum' ) );
132 | $ret = $tmp[0];
133 | }
134 | elseif( $this->commandExists( 'md5sum' ) ) {
135 | $tmp = explode( ' ', exec( 'md5sum ' . $path ) );
136 | $ret = $tmp[0];
137 | } else {
138 | $ret = md5_file( $path );
139 | }
140 |
141 | return $ret;
142 | }
143 |
144 | /**
145 | * Get the changelog URL for the current build
146 | * @return string The changelog URL
147 | */
148 | private function _getChangelogUrl() {
149 | if( file_exists( str_replace( '.zip', '.txt', $this->filePath ) ) )
150 | $ret = str_replace('.zip', '.txt', $this->url);
151 | elseif( file_exists( str_replace( '.zip', '.html', $this->filePath ) ) )
152 | $ret = str_replace( '.zip', '.html', $this->url );
153 | else
154 | $ret = '';
155 |
156 | return $ret;
157 | }
158 |
159 | /**
160 | * Checks if a command is available on the current server
161 | * @param string $cmd The current command to execute
162 | * @return boolean Return True if available, False if not
163 | */
164 | private function commandExists( $cmd ) {
165 | if( ! $this->functionEnabled( 'shell_exec' ) )
166 | return false;
167 |
168 | $returnVal = shell_exec( "which $cmd" );
169 |
170 | return empty( $returnVal ) ? false : true;
171 | }
172 |
173 | /**
174 | * Checks if a php function is available on the server
175 | * @param string $func The function to check for
176 | * @return boolean true if the function is enabled, false if not
177 | */
178 | private function functionEnabled( $func ) {
179 | return is_callable( $func ) && false === stripos( ini_get( 'disable_functions' ), $func );
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/Helpers/Build.php:
--------------------------------------------------------------------------------
1 | model ) {
58 | if( count($params['channels']) > 0 ) {
59 | foreach( $params['channels'] as $channel ) {
60 | if( strtolower($channel) == $this->channel ) $ret = true;
61 | }
62 | }
63 | }
64 |
65 | return $ret;
66 | }
67 |
68 | /* Getters */
69 |
70 | /**
71 | * Return the MD5 value of the current build
72 | * @return string The MD5 hash
73 | */
74 | public function getMD5(){
75 | return $this->md5;
76 | }
77 |
78 | /**
79 | * Get filesize of the current build
80 | * @return string filesize in bytes
81 | */
82 | public function getSize() {
83 | return $this->size;
84 | }
85 |
86 | /**
87 | * Get a unique id of the current build
88 | * @return string A unique id
89 | */
90 | public function getUid() {
91 | return $this->uid;
92 | }
93 |
94 | /**
95 | * Get the Incremental value of the current build
96 | * @return string The incremental value
97 | */
98 | public function getIncremental() {
99 | return $this->incremental;
100 | }
101 |
102 | /**
103 | * Get the API Level of the current build.
104 | * @return string The API Level value
105 | */
106 | public function getApiLevel() {
107 | return $this->apiLevel;
108 | }
109 |
110 | /**
111 | * Get the Url of the current build
112 | * @return string The Url value
113 | */
114 | public function getUrl() {
115 | return $this->url;
116 | }
117 |
118 | /**
119 | * Get the timestamp of the current build
120 | * @return string The timestamp value
121 | */
122 | public function getTimestamp() {
123 | return $this->timestamp;
124 | }
125 |
126 | /**
127 | * Get the changelog Url of the current build
128 | * @return string The changelog Url value
129 | */
130 | public function getChangelogUrl() {
131 | return $this->changelogUrl;
132 | }
133 |
134 | /**
135 | * Get the channel of the current build
136 | * @return string The channel value
137 | */
138 | public function getChannel() {
139 | return $this->channel;
140 | }
141 |
142 | /**
143 | * Get the filename of the current build
144 | * @return string The filename value
145 | */
146 | public function getFilename() {
147 | return $this->filename;
148 | }
149 |
150 | /**
151 | * Get the version of the current build
152 | * @return string the version value
153 | */
154 | public function getVersion() {
155 | return $this->version;
156 | }
157 |
158 | /**
159 | * Export a JSON representation of the object values
160 | * @return string the JSON data
161 | */
162 | public function exportData() {
163 | return get_object_vars( $this );
164 | }
165 |
166 | /**
167 | * Import a JSON representation of the object values
168 | * @param string $data The data to import
169 | * @return object return ourselves
170 | */
171 | public function importData( $data ) {
172 | if( is_array( $data ) ) {
173 | foreach( $data as $key => $value ) {
174 | if( property_exists( $this, $key ) ) {
175 | $this->$key = $value;
176 | }
177 | }
178 | }
179 |
180 | return $this;
181 | }
182 | /**
183 | * Parse a string for the tokens of lineage/cm release archive
184 | * @param type $fileName The filename to be parsed
185 | * @return array The tokens of the filename, both as numeric and named entries
186 | */
187 | public function parseFilenameFull( $fileName ) {
188 | /*
189 | tokens Schema:
190 | array(
191 | 1 => [TYPE] (ex. cm, lineage, etc.)
192 | 2 => [VERSION] (ex. 10.1.x, 10.2, 11, etc.)
193 | 3 => [DATE OF BUILD] (ex. 20140130)
194 | 4 => [CHANNEL OF THE BUILD] (ex. RC, RC2, NIGHTLY, etc.)
195 | 5 =>
196 | CM => [SNAPSHOT CODE] (ex. ZNH0EAO2O0, etc.)
197 | LINEAGE => [MODEL] (ex. i9100, i9300, etc.)
198 | 6 =>
199 | CM => [MODEL] (ex. i9100, i9300, etc.)
200 | LINEAGE => [SIGNED] (ex. signed)
201 | )
202 | */
203 | $tokens = array( 'type' => '', 'version' => '', 'date' => '', 'channel' => '', 'code' => '', 'model' => '', 'signed' => '' );
204 |
205 | preg_match_all( '/([A-Za-z0-9]+)?-([0-9\.]+)-([\d_]+)?-([\w+]+)-([A-Za-z0-9_]+)?-?([\w+]+)?/', $fileName, $tokens );
206 |
207 | $result = $this->removeTrailingDashes( $tokens );
208 |
209 | if( count( $result ) == 7 ) {
210 | $result['type'] = $result[1];
211 | $result['version'] = $result[2];
212 | $result['date'] = $result[3];
213 | $result['channel'] = $result[4];
214 |
215 | if( $result[1] == 'cm' ) {
216 | $result['code'] = $result[5];
217 | $result['model'] = $result[6];
218 | $result['signed'] = false;
219 | } else {
220 | $result['code'] = false;
221 | $result['model'] = $result[5];
222 | $result['signed'] = $result[6];
223 | }
224 | }
225 |
226 | return $result;
227 | }
228 |
229 | /* Utility / Internal */
230 |
231 | /**
232 | * Parse a string for the tokens of lineage/cm delta archive
233 | * @param type $fileName The filename to be parsed
234 | * @return array The tokens of the filename
235 | */
236 | protected function parseFilenameDeltal( $fileName ) {
237 | /*
238 | tokens Schema:
239 | array(
240 | 1 => [SOURCE VERSION] (eng.matthi.20200202.195647)
241 | 2 => [TARGET VERSION] (eng.matthi.20200305.185431)
242 | )
243 | */
244 | preg_match_all( '/([\w+]+)-([\w+]+)/', $fileName, $tokens );
245 | return $this->removeTrailingDashes( tokens );
246 | }
247 |
248 | /**
249 | * Remove trailing dashes
250 | * @param string $token The string where to do the operation
251 | * @return string The string without trailing dashes
252 | */
253 | protected function removeTrailingDashes( $token ) {
254 | foreach( $token as $key => $value ) {
255 | $token[$key] = trim( $value[0], '-' );
256 | }
257 |
258 | return $token;
259 | }
260 |
261 | /**
262 | * Get the current channel of the build based on the current token
263 | * @param string $token The channel obtained from build.prop
264 | * @param string $type The ROM type from filename
265 | * @param string $version The ROM version from filename
266 | * @return string The correct channel to be returned
267 | */
268 | protected function _getChannel( $token, $type, $version ) {
269 | $ret = 'stable';
270 |
271 | $token = strtolower( $token );
272 |
273 | if( $token > '' ) {
274 | $ret = $token;
275 |
276 | if( $token == 'experimental' && ( $type == 'cm' || ( $type == 'lineage' && version_compare ( $version, '14.1', '<' ) ) ) ) $ret = 'snapshot';
277 | if( $token == 'unofficial' && ( $type == 'cm' || ( $type == 'lineage' && version_compare ( $version, '14.1', '<' ) ) ) ) $ret = 'nightly';
278 | }
279 |
280 | return $ret;
281 | }
282 |
283 | /**
284 | * Get the correct URL for the build
285 | * @param string $fileName The name of the file
286 | * @return string The absolute URL for the file to be downloaded
287 | */
288 | protected function _getUrl( $fileName, $basePath ) {
289 | $prop = $this->getBuildPropValue( 'ro.build.ota.url' );
290 |
291 | if( !empty( $prop ) )
292 | return $prop;
293 |
294 | if( empty( $fileName ) ) $fileName = $this->filename;
295 |
296 | return $basePath . '/' . $fileName;
297 | }
298 |
299 | /**
300 | * Get a property value based on the $key value.
301 | * It does it by searching inside the file build.prop of the current build.
302 | * @param string $key The key for the wanted value
303 | * @param string $fallback The fallback value if not found in build.prop
304 | * @return string The value for the specified key
305 | */
306 | protected function getBuildPropValue( $key, $fallback = null ) {
307 | $ret = $fallback ?: null;
308 |
309 | if( $this->buildProp ) {
310 | foreach( $this->buildProp as $line ) {
311 | if( strpos( $line, $key ) !== false ) {
312 | $tmp = explode( '=', $line );
313 | $ret = $tmp[1];
314 |
315 | break;
316 | }
317 | }
318 | }
319 |
320 | return $ret;
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/src/CmOta.php:
--------------------------------------------------------------------------------
1 | initConfig();
43 | $this->initRouting();
44 | $this->initBuilds();
45 | }
46 |
47 | /**
48 | * Get the global configuration
49 | * @return array The whole configuration until this moment
50 | */
51 | public function getConfig() {
52 | return Flight::cfg()->get();
53 | }
54 |
55 | /**
56 | * Set a configuration option based on a key
57 | * @param type $key The key of your configuration
58 | * @param type $value The value that you want to set
59 | * @return class Return always itself, so it can be chained within calls
60 | */
61 | public function setConfig( $key, $value ) {
62 | Flight::cfg()->set( $key, $value );
63 |
64 | return $this;
65 | }
66 |
67 | /**
68 | * Set a configuration option based on a JSON file
69 | * @param type $key The key of your configuration
70 | * @param type $value The file which contents you want to set
71 | * @return class Return always itself, so it can be chained within calls
72 | */
73 | public function setConfigJSON( $key, $file ) {
74 | Flight::cfg()->set( $key, json_decode( file_get_contents( Flight::cfg()->get( 'realBasePath' ) . '/' . $file ) , true ) );
75 |
76 | return $this;
77 | }
78 |
79 | /**
80 | * Set a configuration option based on a JSON file
81 | * @param type $key The key of your configuration
82 | * @param type $value The file which contents you want to set
83 | * @return class Return always itself, so it can be chained within calls
84 | */
85 | public function loadConfigJSON( $file ) {
86 | $settingsFile = Flight::cfg()->get( 'realBasePath' ) . '/' . $file;
87 |
88 | if( file_exists( $settingsFile ) ) {
89 | $settings = json_decode( file_get_contents( $settingsFile ), true );
90 |
91 | if( is_array( $settings ) ) {
92 | foreach( $settings[0] as $key => $value ) {
93 | Flight::cfg()->set( $key, $value );
94 | }
95 | }
96 | }
97 | return $this;
98 | }
99 |
100 | /**
101 | * This initialize the REST API Server
102 | * @return class Return always itself, so it can be chained within calls
103 | */
104 | public function run() {
105 | $loader = new \Twig\Loader\FilesystemLoader( Flight::cfg()->get( 'realBasePath' ) . '/views' );
106 |
107 | $twigConfig = array();
108 |
109 | Flight::register( 'twig', '\Twig\Environment', array( $loader, array() ), function ($twig) {
110 | // Nothing to do here
111 | });
112 |
113 | Flight::start();
114 |
115 | return $this;
116 | }
117 |
118 | /* Utility / Internal */
119 |
120 | // Used to compare timestamps in the build ksort call inside of initRouting for the "/" route
121 | private function compareByTimeStamp( $a, $b ) {
122 | return $a['timestamp'] - $b['timestamp'];
123 | }
124 |
125 | // Format a file size string nicely
126 | private function formatFileSize( $bytes, $dec = 2 ) {
127 | $size = array( ' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB' );
128 | $factor = floor( ( strlen( $bytes ) - 1 ) / 3 );
129 |
130 | return sprintf( "%.{$dec}f", $bytes / pow( 1024, $factor ) ) . @$size[$factor];
131 | }
132 |
133 | // Setup Flight's routing information
134 | private function initRouting() {
135 | // Just list the builds folder for now
136 | Flight::route('/', function() {
137 | // Get the template name we're going to use and tack on .twig
138 | $templateName = Flight::cfg()->get( 'OTAListTemplate' ) . '.twig';
139 |
140 | // Make sure the template exists, otherwise fall back to our default
141 | if( ! file_exists( 'views/' . $templateName ) ) { $templateName = 'ota-list-tables.twig'; }
142 |
143 | // Time to setup some variables for use later.
144 | $builds = Flight::builds()->get();
145 | $buildsToSort = array();
146 | $output = '';
147 | $model = 'Unknown';
148 | $deviceNames = Flight::cfg()->get( 'DeviceNames' );
149 | $vendorNames = Flight::cfg()->get( 'DeviceVendors' );
150 | $devicesByVendor = array();
151 | $parsedFilenames = array();
152 | $formatedFileSizes = array();
153 | $githubURL = '';
154 |
155 | if( ! is_array( $deviceNames ) ) { $deviceNames = array(); }
156 |
157 | // Loop through the builds to do some setup work
158 | foreach( $builds as $build ) {
159 | // Split the filename using the parser in the build class to get some details
160 | $filenameParts = Flight::build()->parseFilenameFull( $build['filename'] );
161 |
162 | // Same the parsed filesnames for later use in the template
163 | $parsedFilenames[$build['filename']] = $filenameParts;
164 |
165 | // In case no Github URL was configured, see if we can get it from an existing Github repo
166 | if( $githubURL == '' && strstr( $build['url'], 'github.com' ) ) {
167 | $path = parse_url( $build['url'], PHP_URL_PATH );
168 | $pathParts = explode( '/', $path );
169 | $githubURL = 'https://github.com/' . $pathParts[1];
170 | }
171 |
172 | $formatedFileSizes[$build['filename']] = $this->formatFileSize( $build['size'], 0 );
173 |
174 | // Check to see if the formated size is less than 5 characters, aka 3 for the postfix and
175 | // one for the actual size, if so, let's add some decimal places to it. We want to avoid files
176 | // are close to a single digit size reporting too little info, a 1400 MB file would round down
177 | // to 1 GB, so instead display 1.4 GB.
178 | if( strlen( $formatedFileSizes[$build['filename']] ) < 5 ) {
179 | $formatedFileSizes[$build['filename']] = $this->formatFileSize( $build['size'], 1 );
180 | }
181 |
182 | // Add the build to a list based on model names
183 | $buildsToSort[$filenameParts['model']][] = $build;
184 | }
185 |
186 | // Sort the array based on model name
187 | ksort( $buildsToSort );
188 |
189 | // Sort the entries in each model based on time/date
190 | foreach( $buildsToSort as $model => $sort ) {
191 | usort( $sort, array( $this, 'compareByTimeStamp' ) );
192 | }
193 |
194 | // Create a list of vendors and the devices that belong to them
195 | foreach( $vendorNames as $model => $vendor ) {
196 | $devicesByVendor[$vendor][] = $model;
197 | }
198 |
199 | // Sort the vendor names
200 | ksort( $devicesByVendor );
201 |
202 | // Sort the devices for each vendor
203 | foreach( $devicesByVendor as $vendor => $devices ) {
204 | sort( $devices );
205 | }
206 |
207 | // Setup branding information for the template
208 | $branding = array( 'name' => Flight::cfg()->get( 'BrandName' ),
209 | 'GithubURL' => Flight::cfg()->get( 'GithubHomeURL' ),
210 | 'LocalURL' => Flight::cfg()->get( 'LocalHomeURL' )
211 | );
212 |
213 | // Sanity check the branding, use some reasonable deductions if anything is missing
214 | if( $branding['name'] == '' && is_array( $parsedFilenames ) ) { $branding['name'] = reset( $parsedFilenames )['type']; }
215 | if( $branding['GithubURL'] == '' ) { $branding['GithubURL'] = $githubURL; }
216 | if( $branding['LocalURL'] == '' ) { $branding['LocalURL'] = Flight::cfg()->get( 'basePath' ) . '/builds'; }
217 |
218 | // Render the template with Twig
219 | Flight::twig()->display( $templateName,
220 | array( 'builds' => $builds,
221 | 'sortedBuilds' => $buildsToSort,
222 | 'parsedFilenames' => $parsedFilenames,
223 | 'deviceNames' => $deviceNames,
224 | 'vendorNames' => $vendorNames,
225 | 'devicesByVendor' => $devicesByVendor,
226 | 'branding' => $branding,
227 | 'formatedFileSizes' => $formatedFileSizes,
228 | )
229 | );
230 | });
231 |
232 | // Main call
233 | Flight::route( '/api', function() {
234 | $ret = array(
235 | 'id' => null,
236 | 'result' => Flight::builds()->get(),
237 | 'error' => null
238 | );
239 |
240 | Flight::json( $ret );
241 | });
242 |
243 | // Delta updates call
244 | Flight::route( '/api/v1/build/get_delta', function() {
245 | $ret = array();
246 |
247 | $delta = Flight::builds()->getDelta();
248 |
249 | if ( $delta === false ) {
250 | $ret['errors'] = array(
251 | 'message' => 'Unable to find delta'
252 | );
253 | } else {
254 | $ret = array_merge( $ret, $delta );
255 | }
256 |
257 | Flight::json($ret);
258 | });
259 |
260 | // LineageOS new API
261 | Flight::route( '/api/v1/@deviceType(/@romType(/@incrementalVersion))', function ( $deviceType, $romType, $incrementalVersion ) {
262 | Flight::builds()->setPostData(
263 | array(
264 | 'params' => array(
265 | 'device' => $deviceType,
266 | 'channels' => array(
267 | $romType,
268 | ),
269 | 'source_incremental' => $incrementalVersion,
270 | ),
271 | )
272 | );
273 |
274 | $ret = array(
275 | 'id' => null,
276 | 'response' => Flight::builds()->get(),
277 | 'error' => null
278 | );
279 |
280 | Flight::json( $ret );
281 | });
282 | }
283 |
284 | /**
285 | * Register the config array within Flight
286 | */
287 | private function initConfig() {
288 | Flight::register( 'cfg', '\DotNotation', array(), function( $cfg ) {
289 | $cfg->set( 'basePath', '' );
290 | $cfg->set( 'realBasePath', realpath( __DIR__ . '/..' ) );
291 | });
292 | }
293 |
294 | /**
295 | * Register the build class within Flight
296 | */
297 | private function initBuilds() {
298 | Flight::register( 'builds', '\JX\CmOta\Helpers\Builds', array(), function( $builds ) {
299 | // Do nothing for now
300 | });
301 |
302 | Flight::register( 'build', '\JX\CmOta\Helpers\Build', array(), function( $build ) {
303 | // Do nothing for now
304 | });
305 | }
306 | }
307 |
--------------------------------------------------------------------------------
/src/Helpers/Builds.php:
--------------------------------------------------------------------------------
1 | set( 'buildsPath', Flight::cfg()->get( 'basePath' ) . '/builds/full' );
46 | Flight::cfg()->set( 'deltasPath', Flight::cfg()->get( 'basePath' ) . '/builds/delta' );
47 |
48 | // Get the current POST request data
49 | $this->postData = Flight::request()->data;
50 | }
51 |
52 | /**
53 | * Return a valid response list of builds available based on the current request
54 | * @return array An array preformatted with builds
55 | */
56 | public function get() {
57 | // Time to get the builds.
58 | $this->builds = array();
59 | $this->getBuildsLocal();
60 | $this->getBuildsGithub();
61 |
62 | $ret = array();
63 |
64 | foreach( $this->builds as $build ) {
65 | array_push( $ret,
66 | array(
67 | // CyanogenMod
68 | 'incremental' => $build->getIncremental(),
69 | 'api_level' => $build->getApiLevel(),
70 | 'url' => $build->getUrl(),
71 | 'timestamp' => $build->getTimestamp(),
72 | 'md5sum' => $build->getMD5(),
73 | 'changes' => $build->getChangelogUrl(),
74 | 'channel' => $build->getChannel(),
75 | 'filename' => $build->getFilename(),
76 | // LineageOS
77 | 'romtype' => $build->getChannel(),
78 | 'datetime' => $build->getTimestamp(),
79 | 'version' => $build->getVersion(),
80 | 'id' => $build->getUid(),
81 | 'size' => $build->getSize(),
82 | )
83 | );
84 | }
85 |
86 | return $ret;
87 | }
88 |
89 | /**
90 | * Set a custom set of POST data. Useful to hack the flow in case the data doesn't come within the body of the HTTP request
91 | * @param array An array structured as POST data
92 | * @return void
93 | */
94 | public function setPostData( $customData ) {
95 | $this->postData = $customData;
96 | }
97 |
98 | /**
99 | * Return a valid response of the delta build (if available) based on the current request
100 | * @return array An array preformatted with the delta build
101 | */
102 | public function getDelta() {
103 | $ret = false;
104 |
105 | $source = $this->postData['source_incremental'];
106 | $target = $this->postData['target_incremental'];
107 |
108 | if( $source != $target ) {
109 | $sourceToken = null;
110 | foreach( $this->builds as $build ) {
111 | if( $build->getIncremental() == $target ) {
112 | $delta = $sourceToken->getDelta( $build );
113 | $ret = array(
114 | 'date_created_unix' => $delta['timestamp'],
115 | 'filename' => $delta['filename'],
116 | 'download_url' => $delta['url'],
117 | 'api_level' => $delta['api_level'],
118 | 'md5sum' => $delta['md5'],
119 | 'incremental' => $delta['incremental']
120 | );
121 | } elseif( $build->getIncremental() == $source ) {
122 | $sourceToken = $build;
123 | }
124 | }
125 | }
126 |
127 | return $ret;
128 | }
129 |
130 | /* Utility / Internal */
131 |
132 | private function getBuildsLocal() {
133 | // Check to see if local builds are disabled in the config file.
134 | if( Flight::cfg()->get('DisableLocalBuilds') == true ) {
135 | return;
136 | }
137 |
138 | // Check to see if we have a cached version of the local builds that is less than a day old
139 | $cacheFilename = Flight::cfg()->get('realBasePath') . '/local.cache.json';
140 | $cacheEnabled = Flight::cfg()->get('EnableLocalCache') == true ? true : false;
141 | $cacheTimeout = Flight::cfg()->get('LocalCacheTimeout');
142 |
143 | if( $cacheTimeout < 1 ) { $cacheTimout = 86400; }
144 |
145 | if( $cacheEnabled && file_exists( $cacheFilename ) && filesize( $cacheFilename ) > 0 && ( time() - filemtime( $cacheFilename ) < $cacheTimeout ) ) {
146 | $data_set = json_decode( file_get_contents( $cacheFilename ) , true );
147 |
148 | foreach( $data_set as $build_data ) {
149 | $build = new BuildLocal( '', '', $build_data );
150 |
151 | if( $build->isValid( $this->postData['params'] ) ) {
152 | array_push( $this->builds, $build );
153 | }
154 | }
155 | } else {
156 | // Get physical paths of where the files resides
157 | $path = Flight::cfg()->get( 'realBasePath' ) . '/builds/full';
158 |
159 | // Get subdirs
160 | $dirs = glob( $path . '/*' , GLOB_ONLYDIR );
161 | array_push( $dirs, $path );
162 |
163 | // Setup a cache array so we can store the local releases separately from the other release types
164 | $localBuilds = array();
165 |
166 | foreach( $dirs as $dir ) {
167 | // Get the file list and parse it
168 | $files = scandir( $dir );
169 |
170 | if( count( $files ) > 0 ) {
171 | foreach( $files as $file ) {
172 | $extension = pathinfo( $file, PATHINFO_EXTENSION );
173 |
174 | if( $extension == 'zip' ) {
175 | $build = null;
176 |
177 | // If APC is enabled
178 | if( extension_loaded( 'apcu' ) && ini_get( 'apc.enabled' ) ) {
179 | $build = apcu_fetch( $file );
180 |
181 | // If not found there, we have to find it with the old fashion method...
182 | if( $build === FALSE ) {
183 | $build = new BuildLocal( $file, $dir );
184 | // ...and then save it for 72h until it expires again
185 | apcu_store( $file, $build, 72*60*60 );
186 | }
187 | } else
188 | $build = new BuildLocal( $file, $dir );
189 |
190 | // Store this build to the cache
191 | if( $cacheEnabled ) {
192 | array_push( $localBuilds, $build->exportData() );
193 | }
194 |
195 | if ( $build->isValid( $this->postData['params'] ) ) {
196 | array_push( $this->builds , $build );
197 | }
198 | }
199 | }
200 | }
201 | }
202 |
203 | // Store the local releases to the cache file
204 | if( $cacheEnabled ) {
205 | file_put_contents( $cacheFilename, json_encode( $localBuilds, JSON_PRETTY_PRINT ) );
206 | }
207 | }
208 | }
209 |
210 | private function getBuildsGithub() {
211 | // Check to see if Github builds are disabled in the config file.
212 | if( Flight::cfg()->get( 'DisableGithubBuilds' ) == true ) {
213 | return;
214 | }
215 |
216 | $cacheFilename = Flight::cfg()->get( 'realBasePath' ) . '/github.cache.json';
217 | $cacheEnabled = Flight::cfg()->get( 'EnableGithubCache' ) == false ? false : true;
218 | $cacheTimeout = Flight::cfg()->get( 'GithubCacheTimeout' );
219 |
220 | if( $cacheTimeout < 1 ) { $cacheTimout = 86400; }
221 |
222 | // Check to see if caching is enabled and we have a cached version of the Github builds that is less than a day old
223 | if( $cacheEnabled && file_exists( $cacheFilename ) && filesize( $cacheFilename ) > 0 && ( time() - filemtime( $cacheFilename ) < $cacheTimeout ) ) {
224 | $data_set = json_decode( file_get_contents( $cacheFilename ) , true );
225 |
226 | foreach( $data_set as $build_data ) {
227 | $build = new BuildGithub( array(), $build_data );
228 |
229 | if ( $build->isValid( $this->postData['params'] ) ) {
230 | array_push( $this->builds, $build );
231 | }
232 | }
233 | } else {
234 | // Get Repos with potential OTA releases
235 | $repos = Flight::cfg()->get( 'githubRepos' );
236 |
237 | // Setup a cache array so we can store the Github releases separately from the other release types
238 | $githubBuilds = array();
239 |
240 | // Get the max releases per repo from the config
241 | $maxReleases = Flight::cfg()->get( 'MaxGithubReleasesPerRepo' );
242 |
243 | // If maxReleases wasn't set, or set to 0, use a really big number for our maximum releases
244 | if( $maxReleases < 1 ) { $maxReleases = PHP_INT_MAX; }
245 |
246 | // Get the max age for releases from the config
247 | $maxAge = strtotime( Flight::cfg()->get( 'OldestGithubRelease' ) );
248 |
249 | foreach( $repos as $repo ) {
250 | // The Github API limits results to 100 at a time, so we may have to go through multiple pages to get
251 | // all of the releases, so setup a page counter before we begin.
252 | $pageCount = 1;
253 | $releaseCount = 0;
254 |
255 | while( $pageCount != false ) {
256 | $request = new CurlRequest( 'https://api.github.com/repos/' . $repo['name'] . '/releases?per_page=100&page=' . $pageCount );
257 | $request->addHeader( 'Accept: application/vnd.github.v3+json' );
258 |
259 | if( $request->executeRequest() ) {
260 | $releases = json_decode( $request->getResponse(), true );
261 |
262 | // If we received less than 100 results, there are no more pages so we can exit the loop,
263 | // otherwise increase out page count and get some more releases.
264 | if( count( $releases ) < 100 ) { $pageCount = false; } else { $pageCount++; }
265 |
266 | foreach( $releases as $release ) {
267 | // Bump our release counter for this repo
268 | $releaseCount++;
269 |
270 | // Check to see if we're reached out maximum release count yet, if so we can exit the
271 | // loop and not get any more results.
272 | if( $releaseCount > $maxReleases ) {
273 | $pageCount = false;
274 |
275 | break 1;
276 | }
277 |
278 | // Check to see if this release is older than our max release age, if so we can skip it.
279 | if( strtotime( $release['published_at'] ) >= $maxAge ) {
280 | $build = new BuildGithub( $release );
281 |
282 | // Store this build to the cache
283 | if( $cacheEnabled ) {
284 | array_push( $githubBuilds, $build->exportData() );
285 | }
286 |
287 | if ( $build->isValid( $this->postData['params'] ) ) {
288 | array_push( $this->builds, $build );
289 | }
290 | }
291 | }
292 | }
293 | }
294 | }
295 |
296 | // Store the Github releases to the cache file
297 | if( $cacheEnabled ) {
298 | file_put_contents( $cacheFilename, json_encode( $githubBuilds, JSON_PRETTY_PRINT ) );
299 | }
300 | }
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LineageOTA
2 | A simple OTA REST Server for LineageOS OTA Updater System Application
3 |
4 | ## Support
5 |
6 | Got a question? Not sure where it should be made? See [CONTRIBUTING](CONTRIBUTING.md).
7 |
8 | ## Contents
9 | * [Requirements](#requirements)
10 | * [How to use](#how-to-use)
11 | * [Local Hosting](#local-hosting)
12 | * [Github Hosting](#github-hosting)
13 | * [Disabling Local/Github Hosting](#disabling-localgithub-hosting)
14 | * [Limiting Github Releases](#limiting-github-releases)
15 | * [Caching](#caching)
16 | * [Web Root Templates](#web-root-templates)
17 | * [REST Server Unit Testing](#rest-server-unit-testing)
18 | * [ROM Integration](#rom-integration)
19 | * [Changelog](#changelog)
20 |
21 | ## Requirements
22 |
23 | - Apache mod_rewrite enabled
24 | - PHP >= 8.2
25 | - PHP ZIP Extension
26 | - Composer ( if installing via CLI )
27 |
28 | ## How to use
29 |
30 | ### Composer
31 |
32 | ```shell
33 | $ cd /var/www/html # Default Apache WWW directory, feel free to choose your own
34 | $ composer create-project julianxhokaxhiu/lineage-ota LineageOTA
35 | ```
36 |
37 | then finally visit http://localhost/LineageOTA to see the REST Server up and running. Please note that this is only for a quick test, when you plan to use that type of setup for production (your users), make sure to also provide HTTPS support.
38 |
39 | > If you get anything else then a list of files, contained inside the `builds` directory, this means something is wrong in your environment. Double check it, before creating an issue report here.
40 |
41 | ### Docker
42 |
43 | ```shell
44 | $ docker run \
45 | --restart=always \
46 | -d \
47 | -p 80:80 \
48 | -v "/home/user/builds:/var/www/html/builds/full" \
49 | julianxhokaxhiu/lineageota
50 | ```
51 |
52 | then finally visit http://localhost/ to see the REST Server up and running.
53 |
54 | The root URL (used to generate ROM URLs in the `/api` endpoint) can be set using the `LINEAGEOTA_BASE_PATH` variable.
55 |
56 | ## Local Hosting
57 |
58 | - Full builds should be uploaded into `builds/full` directory.
59 | - Delta builds should be uploaded into `builds/delta` directory.
60 |
61 | ### ONLY for LineageOS 15.x and newer
62 |
63 | If you are willing to use this project on top of your LineageOS 15.x ( or newer ) ROM builds, you may have noticed that the file named `build.prop` have been removed inside your ZIP file, and has been instead integrated within your `system.new.dat` file, which is basically an ext4 image ( you can find out more here: https://source.android.com/devices/tech/ota/block ).
64 |
65 | In order to make use of this Server from now on, you **MAY** copy the `build.prop` file from your build directory ( where your ROM is being built ), inside the same directory of your ZIP and name it like your ZIP file name + the `.prop` extension.
66 |
67 | For example, feel free to check this structure:
68 |
69 | ```shell
70 | $ cd builds/full
71 | $ tree
72 | .
73 | ├── lineage-15.0-20171030-NIGHTLY-gts210vewifi.zip # the full ROM zip file
74 | └── lineage-15.0-20171030-NIGHTLY-gts210vewifi.zip.prop # the ROM build.prop file
75 | ```
76 |
77 | ### What happens if no build.prop file is found
78 |
79 | The Server is able to serve the ZIP file via the API, also when a `build.prop` file is not given, by fetching those missing informations elsewhere ( related always to that ZIP file ). Although, as it's a trial way, it may be incorrect so don't rely too much on it.
80 |
81 | I am not sure how much this may help anyway, but this must be used as an extreme fallback scenario where you are not able to provide a `build.prop` for any reason. Instead, please always consider to find a way to obtain the prop file, in order to deliver a proper API response.
82 |
83 | ## Github Hosting
84 |
85 | If you want to host your roms on Github you can put your repository names inside the [`github.json`](github.json) file, like this example below:
86 | ```json
87 | [
88 | {
89 | "name": "ADeadTrousers/android_device_Unihertz_Atom_XL_EEA",
90 | "name": "ADeadTrousers/android_device_Unihertz_Atom_XL_TEE"
91 | }
92 | ]
93 | ```
94 |
95 | Each line should point to a repository for a single device and have Github releases with attached files. At a minimum there should be the following files in each release:
96 |
97 | * build.prop
98 | * OTA release zip
99 | * .md5sum list
100 |
101 | The md5sum file contains a list of hash values for the the OTA zip as well as any other files you included in the release that need them. Each line of the md5sum should be of the format:
102 |
103 | ```
104 | HASHVALUE FILENAME
105 | ```
106 |
107 | The filename should not contain any directory information.
108 |
109 | You may also include a changelog file in html format. Note, any html file included in the release file list will be included as a changelog.
110 |
111 | ## Disabling Local/Github Hosting
112 |
113 | Both local and Github hosting features can be disable if they are not being used via the configuration file, in the root directory, called lineageota.json:
114 |
115 | ```json
116 | [
117 | {
118 | "DisableLocalBuilds": false,
119 | "DisableGithubBuilds": false,
120 | }
121 | ]
122 |
123 | ```
124 |
125 | Setting either of these to true will disable the related hosting option.
126 |
127 | ## Limiting Github Releases
128 |
129 | With Github you may end up having many more releases than the updater really needs to know about, as such there are two options in the config file to let you control the number of releases that are returned:
130 |
131 | ```json
132 | [
133 | {
134 | "MaxGithubReleasesPerRepo": 0,
135 | "OldestGithubRelease": "",
136 | }
137 | ]
138 | ```
139 |
140 | MaxGithubReleaesPerRepo will limit the number of releases used on a per repo basis. Setting this to 0 or leaving it out of the config file will use all available releases in each repo.
141 |
142 | OldestGithubRelease will exclude any released older than a given date from being available in the updater. This string value can be blank for all releases, or any [```strtotime()```](https://www.php.net/manual/en/datetime.formats.php) compatible string, like "2021-01-01" or "60 days ago".
143 |
144 | ## Caching
145 |
146 | Both local builds and Github based builds can be cached to reduce disk and network traffic. By default, local caching is disabled and Github caching is enabled.
147 |
148 | The default cache timeout is set to one day (86400 seconds).
149 |
150 | You can change this via the configuration file, in the root directory, called lineageota.json:
151 |
152 | ```json
153 | [
154 | {
155 | "EnableLocalCache": false,
156 | "EnableGithubCache": true,
157 | "LocalCacheTimeout": 86400,
158 | "GithubCacheTimeout": 86400
159 | }
160 | ]
161 | ```
162 |
163 | This requires the webserver to have write access to the root directory. If you wish to force a refresh of the releases, simply delete the appropriate cache.json file.
164 |
165 | ## Web Root Templates
166 |
167 | In version 2.9 and prior, if a use visited the web root of the OTA server, they would be redirected to the builds folder. With the introduction of Github hosting, this is no longer a particularly useful destination as they may see no builds hosted locally, or incorrect ones if local hosting has been disabled and the local builds folder has not been cleaned up.
168 |
169 | Releases after 2.9 now use a simple templating system to present a list of builds.
170 |
171 | Four templates are included by default (ota-list-simple, ota-list-tables, ota-list-columns, ota-list-javascript) but you can create your own in the "views" folder to match your branding as required.
172 |
173 | There are several configuration settings for temples as follows:
174 | ```json
175 | "OTAListTemplate": "ota-list-tables",
176 | "BrandName": "",
177 | "LocalHomeURL": "",
178 | "GithubHomeURL": "",
179 | "DeviceNames": {
180 | "kebab": "8T",
181 | "lemonade": "9"
182 | },
183 | "DeviceVendors": {
184 | "kebab": "Oneplus",
185 | "lemonade": "Oneplus"
186 | }
187 | ```
188 |
189 | * OTAListTemplate: the name of the template to use, do not include the file extension.
190 | * BrandName: the name of your ROM, if left empty brand name will be used from the OTA filename.
191 | * LocalHomeURL: Homepage URL for local builds, used in the template file. If left empty https://otaserver/builds URL will be used.
192 | * GithubHomeURL: Homepage URL for Github builds, used in the template file. If left empty the organization URL from any Github repos that are defined will be used.
193 | * DeviceNames: A mapping array between device code names and their proper titles. Values: array( codename => title, ... )
194 | * DeviceVendors: A mapping array between device code names and their vendor names. Values: array( codename => vendor, ... )
195 |
196 | Included Templates:
197 |
198 | * ota-list-simple: a simple header and list of files names, no additional details or links provided.
199 | * ota-list-table: a page containing a seires of tables, one per device, that list in date order all builds for that device. Includes jump lists to find devices, links to local/github pages, dates, versions, md5sums, etc.
200 |
201 | Twig is used as the templating language, see their [documentation](https://twig.symfony.com/doc/3.x/) for more details.
202 |
203 | The following variables are available for templates:
204 |
205 | * builds: An array of builds available, each entry is an array that contains; incremental, api_level, url, timestamp, md5sum, changes, cahnnel, filename, romtype, datetime, version, id, size
206 | * sortedBuilds: The builds array sorted by device name (array key is the device name, each value is as in the builds array)
207 | * parsedFilenames: An array of filenames that have been parsed in to the following tokens; type, version, date, channel, code, model, signed
208 | * deviceNames: An array of device names, each key is the code name, the value is the device name (ie ```array( "kebab" => "8T")```)
209 | * vendorNames: An array of device names, each key is the code name, the value is the vendor name (ie ```array( "kebab" => "Oneplus")```)
210 | * devicesByVendor: A two dimensional array of devices by vendor (ie ```array( "Oneplus" => array( "kebab", "lemonade"))```)
211 | * branding: An array of branding info for the updates, contains; name, GithubURL, LocalURL
212 | * formatedFileSizes: An array of human friendly file sizes for each release file, keyed on filenames (from the builds array), values as strings like "1.1 GB"
213 |
214 | ## REST Server Unit Testing
215 |
216 | Feel free to use this [simple script](https://github.com/julianxhokaxhiu/LineageOTAUnitTest) made with NodeJS. Instructions are included.
217 |
218 | ## ROM Integration
219 |
220 | In order to integrate this REST Server within your ROM you have two possibilities: you can make use of the `build.prop` ( highly suggested ), or you can patch directly the `android_packages_apps_CMUpdater` package ( not suggested ).
221 |
222 | > Before integrating, make sure your OTA Server answers from a public URL. Also, make sure to know which is your path.
223 | >
224 | > For eg. if your URL is http://my.ota.uri/LineageOTA, then your API URL will be http://my.ota.uri/LineageOTA/api
225 |
226 | ### Build.prop
227 |
228 | #### CyanogenMod / LineageOS ( <= 14.x )
229 |
230 | In order to integrate this in your CyanogenMod based ROM, you need to add the `cm.updater.uri` property ( for [CyanogenMod](https://github.com/CyanogenMod/android_packages_apps_CMUpdater/blob/cm-14.1/src/com/cyanogenmod/updater/service/UpdateCheckService.java#L206) or [Lineage](https://github.com/LineageOS/android_packages_apps_Updater/blob/cm-14.1/src/org/lineageos/updater/misc/Constants.java#L39) ) in your `build.prop` file. See this example:
231 |
232 | ```properties
233 | # ...
234 | cm.updater.uri=http://my.ota.uri/api/v1/{device}/{type}/{incr}
235 | # ...
236 | ```
237 |
238 | > As of [e930cf7](https://github.com/LineageOS/android_packages_apps_Updater/commit/e930cf7f67d10afcd933dec75879426126d8579a):
239 | > Optional placeholders replaced at runtime:
240 | > {device} - Device name
241 | > {type} - Build type
242 | > {incr} - Incremental version
243 |
244 | #### LineageOS ( >= 15.x)
245 |
246 | In order to integrate this in your LineageOS based ROM, you need to add the [`lineage.updater.uri`](https://github.com/LineageOS/android_packages_apps_Updater/blob/lineage-15.0/src/org/lineageos/updater/misc/Constants.java#L39) property in your `build.prop` file. See this example:
247 |
248 | ```properties
249 | # ...
250 | lineage.updater.uri=https://my.ota.uri/api/v1/{device}/{type}/{incr}
251 | # ...
252 | ```
253 |
254 | Make always sure to provide a HTTPS based uri, otherwise the updater will reject to connect with your server! This is caused by the security policies newer versions of Android (at least 10+) include, as any app wanting to use non-secured connections must explicitly enable this during the compilation. The LineageOS Updater does not support that.
255 |
256 | > Since https://review.lineageos.org/#/c/191274/ is merged, the property `cm.updater.uri` is renamed to `lineage.updater.uri`. Make sure to update your entry.
257 |
258 | > As of [5252d60](https://github.com/LineageOS/android_packages_apps_Updater/commit/5252d606716c3f8d81617babc1293c122359a94d):
259 | > Optional placeholders replaced at runtime:
260 | > {device} - Device name
261 | > {type} - Build type
262 | > {incr} - Incremental version
263 |
264 |
265 | ### android_packages_apps_CMUpdater
266 |
267 | In order to integrate this in your [CyanogenMod](https://github.com/lineageos/android_packages_apps_CMUpdater/blob/cm-14.1/res/values/config.xml#L12) or [LineageOS](https://github.com/LineageOS/android_packages_apps_Updater/blob/cm-14.1/res/values/strings.xml#L29) based ROM, you can patch the relative line inside the package.
268 |
269 | > Although this works ( and the position may change from release to release ), I personally do not suggest to use this practice as it will always require to override this through the manifest, or maintain the commits from the official repo to your fork.
270 | >
271 | > Using the `build.prop` instead offers an easy and smooth integration, which could potentially be used even in local builds that make use fully of the official repos, but only updates through a local OTA REST Server. For example, by using the [docker-lineage-cicd](https://github.com/julianxhokaxhiu/docker-lineage-cicd) project.
272 |
273 | ## Changelog
274 |
275 | ### v?.?.?
276 | - Added template system for web root ( thanks to @toolstack )
277 | - Added config option to limit the number/age of github releases ( thanks to @toolstack )
278 | - Fixed Github returning only the first 100 releases ( thanks to @toolstack )
279 | - Fixed handling of Github releases that contain multiple zip files ( thanks to @toolstack )
280 | - Added config option to disable build types ( thanks to @toolstack )
281 | - Added config file for caching support ( thanks to @toolstack )
282 | - Added local caching support ( thanks to @toolstack )
283 | - Fixed duplicate build retrievals ( thanks to @toolstack )
284 | - Added Github caching support ( thanks to @toolstack )
285 | - Include github as a source repository ( thanks to @ADeadTrousers )
286 | - Accept LINEAGEOTA_BASE_PATH from environment to set the root URL ( thanks to @CyberShadow )
287 | - Read channel from build.prop ro.lineage.releasetype ( thanks to @tduboys )
288 | - fix loading prop file from alternate location ( thanks to @bananer )
289 | - Support device names with underscores in name extraction ( thanks to @bylaws )
290 | - Fix finding numbers on rom names (thanks to @erfanoabdi )
291 | - Fix loading prop file
292 |
293 | ### v2.9.0
294 | - Add PHP 7.4 compatibility: Prevent null array access on `isValid()` ( thanks to @McNutnut )
295 | - Update RegEx pattern to match more roms than just CM/LineageOS ( thanks to @toolstack )
296 | - Use Forwarded HTTP Extension to determine protocol and host ( thanks to @TpmKranz )
297 | - Add detection of HTTP_X_FORWARDED_* headers ( thanks to @ionphractal )
298 |
299 | ### v2.8.0
300 |
301 | - Use md5sum files if available ( thanks to @jplitza )
302 | - Abort commandExists early if shell_exec is disabled ( thanks to @timschumi )
303 | - Update docs to match new uri formatting ( thanks to @twouters )
304 | - Add size field to JSON ( thanks to @syphyr )
305 |
306 | ### v2.7.0
307 |
308 | - Add support for missing `build.prop` file in LineageOS 15.x builds ( see #36 )
309 | - Provide a proper fallback for values if `build.prop` is missing, making the JSON response acting similar [as if it was there](https://github.com/julianxhokaxhiu/LineageOTA/issues/36#issuecomment-343601224)
310 |
311 | ### v2.6.0
312 |
313 | - Add support for the new filename that UNOFFICIAL builds of LineageOS may get from now ( eg. `lineage-14.1-20171024_123000-nightly-hammerhead-signed.zip`) ( thanks to @brianjmurrell )
314 |
315 | ### v2.5.0
316 |
317 | - Add support for the new Lineage namespace within build.prop ( see https://review.lineageos.org/#/c/191274/ )
318 |
319 | ### v2.4.0
320 | - Add support for the new **id** field for LineageOS ( see #32 )
321 | - Mention the need of the PHP ZIP extension in the README in order to run correctly this software ( see #27 )
322 |
323 | ### v2.3.1
324 | - Fix for "Fix for the timestamp value. Now it inherits the one from the ROM". The order to read this value was before the OTA server was aware of the content of the build.prop. ( thanks to @syphyr )
325 |
326 | ### v2.3.0
327 | - Added support for latest LineageOS ROMs that are using the version field ( see #29 )
328 | - Fix for the timestamp value. Now it inherits the one from the ROM ( see #30 )
329 |
330 | ### v2.2.0
331 | - Honor ro.build.ota.url if present ( thanks to @ontherunvaro )
332 | - Add support for recursive subdirectories for full builds ( thanks to @corna )
333 | - Fix changelog URL generation ( thanks to @corna )
334 | - Add support for HTTPS OTA Url ( thanks to @corna )
335 | - Fix tutorial URL inside the README.md ( thanks to @visi0nary )
336 |
337 | ### v2.1.1
338 | - Extend the legacy updater channel support to any Lineage ROM < 14.1
339 |
340 | ### v2.1.0
341 | - Add support for LineageOS unofficial keyword on API requests
342 | - Drop memcached in favor of APCu. Nothing to configure, it just works :)
343 |
344 | ### v2.0.9
345 | - Removing XDelta3 logic for Delta creation ( see https://forum.xda-developers.com/showthread.php?p=69760632#post69760632 for a described correct process )
346 | - Prevent crash of the OTA system if a file is being accessed meanwhile it is being uploaded
347 |
348 | ### v2.0.8
349 | - Adding support for LineageOS CMUpdater ( this should not break current CM ROMs support, if yes please create an issue! )
350 |
351 | ### v2.0.7
352 | - Renamed the whole project from CyanogenMod to LineageOS
353 | - Added support for LineageOS ( and kept support for current CyanogenMod ROMs, until they will transition to LineageOS)
354 |
355 | ### v2.0.6
356 | - Loop only between .ZIP files! Before even .TXT files were "parsed" which wasted some memory. Avoid this and make the REST server memory friendly :)
357 | - HTML Changelogs! If you will now create a changelog file next to your ZIP file with an HTML extension ( eg. `lineage-14.0-20161230-NIGHTLY-hammerhead.html` ) this will be preferred over .TXT ones! Otherwise fallback to the classic TXT extension ( eg. `lineage-14.0-20161230-NIGHTLY-hammerhead.txt` )
358 |
359 | ### v2.0.5
360 | - Fix the parsing of SNAPSHOT builds
361 |
362 | ### v2.0.4
363 | - Final Fix for TXT and ZIP files in the same directory
364 | - Automatic URL detection for basePath ( no real need to touch it again )
365 | - Delta builds array is now returned correctly
366 |
367 | ### v2.0.3
368 | - Memcached support
369 | - UNOFFICIAL builds support ( they will be set as channel = NIGHTLY )
370 | - Fix Delta Builds path
371 | - Fix internal crash when *.txt files were present inside /builds/full path
372 |
373 | ### v2.0.2
374 | - Fix some breaking changes that will not enable the REST server to work correctly.
375 |
376 | ### v2.0.1
377 | - Excluded hiddens files and autogenerated ones by the OS (for example `.something` or `Thumbs.db`).
378 |
379 | ### v2.0
380 | - Refactored the whole code.
381 | - Now everything is PSR4 compliant.
382 | - Introduced composer.json to make easier the installation of the project.
383 |
384 | ## License
385 | See [LICENSE](https://github.com/julianxhokaxhiu/LineageOTA/blob/2.0/LICENSE).
386 |
387 | Enjoy :)
388 |
--------------------------------------------------------------------------------