├── github.json ├── .gitignore ├── .dockerignore ├── .htaccess ├── views ├── ota-list-simple.twig ├── ota-list-tables.css ├── ota-list-tables.twig ├── ota-list-columns.css ├── ota-list-columns.twig ├── ota-list-javascript.css └── ota-list-javascript.twig ├── CONTRIBUTING.md ├── lineageota.json ├── .github ├── workflows │ ├── prepare.ps1 │ └── main.yml └── FUNDING.yml ├── LICENSE ├── Makefile ├── composer.json ├── Dockerfile ├── index.php ├── src ├── Helpers │ ├── CurlRequest.php │ ├── BuildGithub.php │ ├── BuildLocal.php │ ├── Build.php │ └── Builds.php └── CmOta.php └── README.md /github.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | Dockerfile 3 | builds 4 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule ^(.*)$ index.php [QSA,L] -------------------------------------------------------------------------------- /views/ota-list-simple.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | LineageOTA Builds for {{ branding['name'] }} 4 | 5 | 6 |

Currently available builds for {{ branding['name'] }}

7 | {% for build in builds %} 8 | {{ build['filename'] }}
9 | {% endfor %} 10 | 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issue reporting 2 | 3 | If you need help on the code, or you find any issue that you would like to report, feel free to open an issue on this repository. 4 | 5 | # Support 6 | 7 | If you have any issue on running this product on your machine/environment, feel free to use the dedicated XDA Thread: 8 | 9 | https://forum.xda-developers.com/showthread.php?t=2636334 10 | -------------------------------------------------------------------------------- /lineageota.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DisableLocalBuilds": false, 4 | "DisableGithubBuilds": false, 5 | "MaxGithubReleasesPerRepo": 0, 6 | "OldestGithubRelease": "", 7 | "EnableLocalCache": false, 8 | "EnableGithubCache": true, 9 | "LocalCacheTimeout": 86400, 10 | "GithubCacheTimeout": 86400, 11 | "OTAListTemplate": "ota-list-tables", 12 | "BrandName": "", 13 | "LocalHomeURL": "", 14 | "GithubHomeURL": "", 15 | "DeviceNames": { 16 | "kebab": "8T", 17 | "lemonade": "9" 18 | }, 19 | "DeviceVendors": { 20 | "kebab": "Oneplus", 21 | "lemonade": "Oneplus" 22 | } 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /.github/workflows/prepare.ps1: -------------------------------------------------------------------------------- 1 | if ($env:_BUILD_BRANCH -eq "refs/heads/2.0") 2 | { 3 | $env:_IS_BUILD_CI = "true" 4 | $env:_RELEASE_VERSION = "latest" 5 | } 6 | elseif ($env:_BUILD_BRANCH -like "refs/tags/*") 7 | { 8 | $env:_RELEASE_VERSION = $env:_BUILD_VERSION.Substring(0,$env:_BUILD_VERSION.LastIndexOf('.')) 9 | } 10 | 11 | Write-Output "--------------------------------------------------" 12 | Write-Output "CI: $env:_IS_BUILD_CI" 13 | Write-Output "RELEASE NAME: $env:_RELEASE_NAME" 14 | Write-Output "RELEASE VERSION: $env:_RELEASE_VERSION" 15 | Write-Output "--------------------------------------------------" 16 | 17 | Write-Output "_RELEASE_VERSION=${env:_RELEASE_VERSION}" >> ${env:GITHUB_ENV} 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: julianxhokaxhiu 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Julian Xhokaxhiu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # ENVIRONMENT CONFIGURATION 3 | ############################################################################### 4 | MAKEFLAGS += --no-print-directory 5 | SHELL=/bin/bash 6 | 7 | # Use default as default goal when running 'make' 8 | .PHONY: default 9 | .DEFAULT_GOAL := default 10 | 11 | ############################################################################### 12 | # GOAL PARAMETERS 13 | ############################################################################### 14 | 15 | # Container name 16 | CONTAINER_NAME ?= "julianxhokaxhiu/lineageota" 17 | 18 | # Tag name 19 | TAG_NAME ?= "latest" 20 | 21 | ############################################################################### 22 | # GOALS ( safe defaults ) 23 | ############################################################################### 24 | 25 | default: 26 | @docker build -t $(CONTAINER_NAME):$(TAG_NAME) . 27 | 28 | run: 29 | @docker run --rm=true -it -p 8080:80 -v "$(CURDIR)/builds:/var/www/html/builds" $(CONTAINER_NAME):$(TAG_NAME) 30 | 31 | clean: 32 | @docker rmi $(CONTAINER_NAME):$(TAG_NAME) 33 | -------------------------------------------------------------------------------- /views/ota-list-tables.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-align: center; 3 | } 4 | 5 | h2 { 6 | text-align: center; 7 | margin-bottom: 0px; 8 | } 9 | 10 | h3 { 11 | text-align: center; 12 | margin-top: 0px; 13 | } 14 | 15 | table { 16 | margin-left: auto; 17 | margin-right: auto; 18 | border-collapse: collapse; 19 | } 20 | 21 | th { 22 | vertical-align: bottom; 23 | background: blue; 24 | color: white; 25 | font-weight: bold; 26 | padding: 5px; 27 | border: 0px; 28 | } 29 | 30 | tr:nth-child(even) { 31 | background-color: lightgrey; 32 | } 33 | 34 | td { 35 | padding: 20px; 36 | border: 0px; 37 | } 38 | 39 | table th:nth-child(1) { 40 | text-align: center; 41 | } 42 | 43 | table th:nth-child(2) { 44 | text-align: center; 45 | } 46 | 47 | table th:nth-child(3) { 48 | text-align: left; 49 | } 50 | 51 | table th:nth-child(4) { 52 | text-align: center; 53 | } 54 | 55 | table th:nth-child(5) { 56 | text-align: center; 57 | } 58 | 59 | table td:nth-child(1) { 60 | text-align: center; 61 | } 62 | 63 | table td:nth-child(2) { 64 | text-align: center; 65 | } 66 | 67 | table td:nth-child(4) { 68 | text-align: center; 69 | } 70 | 71 | table td:nth-child(5) { 72 | text-align: center; 73 | } 74 | 75 | ul { 76 | text-align: center; 77 | } 78 | 79 | li { 80 | display: inline-block; 81 | padding-right: 10px; 82 | } 83 | 84 | ul li:not(:first-child)::before { 85 | content: "\00b7 "; 86 | } 87 | 88 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: LineageOTA 2 | 3 | on: 4 | push: 5 | branches: 6 | - '2.0' 7 | tags: 8 | - "*" 9 | pull_request: 10 | branches: 11 | - '2.0' 12 | 13 | env: 14 | _RELEASE_NAME: julianxhokaxhiu/lineageota 15 | _RELEASE_VERSION: v0 16 | _BUILD_VERSION: "2.9.0.${{ github.run_number }}" 17 | _BUILD_BRANCH: "${{ github.ref }}" 18 | 19 | jobs: 20 | LineageOTA: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4.1.0 25 | - name: Prepare environment 26 | run: ".github/workflows/prepare.ps1" 27 | shell: pwsh 28 | - name: Login to Docker Hub 29 | uses: docker/login-action@v3.0.0 30 | with: 31 | username: julianxhokaxhiu 32 | password: "${{ secrets.DOCKERHUB_CI_DOCKER_PASSWORD }}" 33 | - name: Build 34 | uses: docker/build-push-action@v5 35 | with: 36 | tags: ${{ env._RELEASE_NAME }}:${{ env._RELEASE_VERSION }} 37 | - name: Run Trivy vulnerability scanner 38 | uses: aquasecurity/trivy-action@master 39 | with: 40 | image-ref: ${{ env._RELEASE_NAME }}:${{ env._RELEASE_VERSION }} 41 | format: 'table' 42 | exit-code: '1' 43 | ignore-unfixed: true 44 | vuln-type: 'os,library' 45 | severity: 'CRITICAL,HIGH,MEDIUM,LOW' 46 | - name: Push 47 | uses: docker/build-push-action@v5 48 | with: 49 | push: true 50 | tags: ${{ env._RELEASE_NAME }}:${{ env._RELEASE_VERSION }} 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "julianxhokaxhiu/lineage-ota", 3 | "description": "A simple OTA REST Server for LineageOS OTA Updater System Application", 4 | "version": "2.9.0", 5 | "type": "project", 6 | "keywords": [ 7 | "android", 8 | "cyanogenmod", 9 | "lineageos", 10 | "ota", 11 | "rest", 12 | "php" 13 | ], 14 | "homepage": "https://github.com/julianxhokaxhiu/LineageOTA", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Julian Xhokaxhiu", 19 | "email": "info@julianxhokaxhiu.com", 20 | "homepage": "http://julianxhokaxhiu.com", 21 | "role": "Creator/Lead Developer" 22 | } 23 | ], 24 | "repositories": [ 25 | { 26 | "type":"package", 27 | "package": { 28 | "name": "julianxhokaxhiu/dotnotation", 29 | "version": "master", 30 | "source": { 31 | "url": "https://gist.github.com/a6098de64195f604f56a.git", 32 | "type": "git", 33 | "reference":"master" 34 | }, 35 | "autoload": { 36 | "classmap": ["."] 37 | } 38 | } 39 | } 40 | ], 41 | "require": { 42 | "mikecao/flight": "2.*", 43 | "julianxhokaxhiu/dotnotation": "dev-master", 44 | "ext-zip": "*", 45 | "twig/twig": "3.*" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "JX\\CmOta\\": "src/" 50 | } 51 | }, 52 | "replace": { 53 | "julianxhokaxhiu/cyanogenmod-ota":"2.*" 54 | }, 55 | "suggest": { 56 | "ext-zip": "This extension is usually enabled, if not make sure it is. LineageOTA requires it to run." 57 | }, 58 | "scripts": { 59 | "serve": "php -S 0.0.0.0:8000" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2-apache 2 | MAINTAINER Julian Xhokaxhiu 3 | 4 | # internal variables 5 | ENV HTML_DIR /var/www/html 6 | ENV FULL_BUILDS_DIR $HTML_DIR/builds/full 7 | 8 | # set the working directory 9 | WORKDIR $HTML_DIR 10 | 11 | # enable mod_rewrite 12 | RUN a2enmod rewrite 13 | 14 | # install the PHP extensions we need 15 | RUN apt-get update \ 16 | && buildDeps=" \ 17 | zlib1g-dev \ 18 | libzip-dev \ 19 | " \ 20 | && apt-get install -y git libzip4 $buildDeps --no-install-recommends \ 21 | && rm -r /var/lib/apt/lists/* \ 22 | \ 23 | && docker-php-ext-install zip \ 24 | \ 25 | && pecl install apcu \ 26 | && docker-php-ext-enable apcu \ 27 | \ 28 | && docker-php-source delete \ 29 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $buildDeps 30 | 31 | # set recommended settings for APCu 32 | # see http://php.net/manual/en/apcu.configuration.php 33 | RUN { \ 34 | echo 'apc.ttl=7200'; \ 35 | } > /usr/local/etc/php/conf.d/opcache-recommended.ini 36 | 37 | # install latest version of composer 38 | ADD https://getcomposer.org/composer.phar /usr/local/bin/composer 39 | RUN chmod 0755 /usr/local/bin/composer 40 | 41 | # add all the project files 42 | COPY . $HTML_DIR 43 | 44 | # enable indexing for Apache 45 | RUN sed -i "1s;^;Options +Indexes\n\n;" .htaccess 46 | 47 | # install dependencies 48 | RUN composer install --no-plugins --no-scripts 49 | 50 | # fix permissions 51 | RUN chmod -R 0775 /var/www/html \ 52 | && chown -R www-data:www-data /var/www/html 53 | 54 | # create volumes 55 | VOLUME $FULL_BUILDS_DIR 56 | -------------------------------------------------------------------------------- /views/ota-list-tables.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | LineageOTA Builds for {{ branding['name'] }} 4 | 5 | 6 | 7 |

Currently available builds for {{ branding['name'] }}

8 | 17 | 18 |


19 | 20 | {% for model,builds in sortedBuilds %} 21 |

{{ model }}

22 | {% if deviceNames[model] is defined %} 23 |

({{ vendorNames[model] }} {{ deviceNames[model] }})

24 | {% endif %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% for build in builds %} 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% if build['url'] matches '/github\.com/' %} 46 | 47 | {% else %} 48 | 49 | {% endif %} 50 | 51 | 52 | 53 | {% endfor %} 54 | 55 |
ChannelVersionFilenameSizeSourceDate
(Y//M/D)
{{ build['channel'] }}{{ build['version'] }}{{ build['filename'] }}
[md5sum: {{ build['md5sum'] }}]
{% set filename = build['filename'] %}{{ formatedFileSizes[filename] }}Githublocal{{ build['timestamp'] | date('Y/m/d') }}
56 | 57 |


58 | 59 | {% endfor %} 60 | 61 | 62 | -------------------------------------------------------------------------------- /views/ota-list-columns.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin-top: 0px; 3 | 4 | } 5 | h1 { 6 | text-align: center; 7 | } 8 | 9 | h2 { 10 | text-align: center; 11 | margin-bottom: 0px; 12 | } 13 | 14 | h3 { 15 | text-align: center; 16 | margin-top: 0px; 17 | } 18 | 19 | table { 20 | margin-left: auto; 21 | margin-right: auto; 22 | border-collapse: collapse; 23 | } 24 | 25 | th { 26 | vertical-align: bottom; 27 | background: blue; 28 | color: white; 29 | font-weight: bold; 30 | padding: 5px; 31 | border: 0px; 32 | } 33 | 34 | tr:nth-child(even) { 35 | background-color: lightgrey; 36 | } 37 | 38 | td { 39 | padding: 20px; 40 | border: 0px; 41 | } 42 | 43 | table th:nth-child(1) { 44 | text-align: center; 45 | } 46 | 47 | table th:nth-child(2) { 48 | text-align: center; 49 | } 50 | 51 | table th:nth-child(3) { 52 | text-align: left; 53 | } 54 | 55 | table th:nth-child(4) { 56 | text-align: center; 57 | } 58 | 59 | table th:nth-child(5) { 60 | text-align: center; 61 | } 62 | 63 | table td:nth-child(1) { 64 | text-align: center; 65 | } 66 | 67 | table td:nth-child(2) { 68 | text-align: center; 69 | } 70 | 71 | table td:nth-child(4) { 72 | text-align: center; 73 | } 74 | 75 | table td:nth-child(5) { 76 | text-align: center; 77 | } 78 | 79 | #devicelist > ul > li { 80 | padding-top: 10px; 81 | } 82 | 83 | li { 84 | list-style-type: none; 85 | font-size: large; 86 | } 87 | 88 | #topbar { 89 | position: sticky; 90 | top: 0; 91 | background-color: blue; 92 | padding: 5px; 93 | font-size: 15px; 94 | color: white; 95 | display: block; 96 | } 97 | 98 | #container { 99 | display: flex; 100 | flex-direction: row; 101 | } 102 | 103 | #devicelist { 104 | padding-right: 10px; 105 | background-color: blue; 106 | color: white; 107 | } 108 | 109 | #devicelist a:visited, 110 | #devicelist a:link, 111 | #devicelist a:hover, 112 | #devicelist a:active { 113 | color: white; 114 | } 115 | 116 | #buildlist { 117 | flex: 1; 118 | width: auto; 119 | } -------------------------------------------------------------------------------- /views/ota-list-columns.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | LineageOTA Builds for {{ branding['name'] }} 4 | 5 | 6 | 7 |
8 |

Currently available builds for {{ branding['name'] }}

9 |
10 |
11 |
12 |
    13 | {% for vendor,models in devicesByVendor %} 14 |
  • {{ vendor }}
  • 15 | {% for model in models %} 16 | 19 | {% endfor %} 20 | {% endfor %} 21 |
22 |
23 |
24 | {% for model,builds in sortedBuilds %} 25 |

{{ model }}

26 | {% if deviceNames[model] is defined %} 27 |

({{ vendorNames[model] }} {{ deviceNames[model] }})

28 | {% endif %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for build in builds %} 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% if build['url'] matches '/github\.com/' %} 50 | 51 | {% else %} 52 | 53 | {% endif %} 54 | 55 | 56 | 57 | {% endfor %} 58 | 59 |
ChannelVersionFilenameSizeSourceDate
(Y//M/D)
{{ build['channel'] }}{{ build['version'] }}{{ build['filename'] }}
[md5sum: {{ build['md5sum'] }}]
{% set filename = build['filename'] %}{{ formatedFileSizes[filename] }}Githublocal{{ build['timestamp'] | date('Y/m/d') }}
60 | 61 | {% endfor %} 62 | 63 |
64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /views/ota-list-javascript.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin-top: 0px; 3 | 4 | } 5 | h1 { 6 | text-align: center; 7 | } 8 | 9 | h2 { 10 | text-align: center; 11 | margin-bottom: 0px; 12 | } 13 | 14 | h3 { 15 | text-align: center; 16 | margin-top: 0px; 17 | } 18 | 19 | table { 20 | margin-left: auto; 21 | margin-right: auto; 22 | border-collapse: collapse; 23 | } 24 | 25 | th { 26 | vertical-align: bottom; 27 | background: blue; 28 | color: white; 29 | font-weight: bold; 30 | padding: 5px; 31 | border: 0px; 32 | } 33 | 34 | tr:nth-child(even) { 35 | background-color: lightgrey; 36 | } 37 | 38 | td { 39 | padding: 20px; 40 | border: 0px; 41 | } 42 | 43 | table th:nth-child(1) { 44 | text-align: center; 45 | } 46 | 47 | table th:nth-child(2) { 48 | text-align: center; 49 | } 50 | 51 | table th:nth-child(3) { 52 | text-align: left; 53 | } 54 | 55 | table th:nth-child(4) { 56 | text-align: center; 57 | } 58 | 59 | table th:nth-child(5) { 60 | text-align: center; 61 | } 62 | 63 | table td:nth-child(1) { 64 | text-align: center; 65 | } 66 | 67 | table td:nth-child(2) { 68 | text-align: center; 69 | } 70 | 71 | table td:nth-child(4) { 72 | text-align: center; 73 | } 74 | 75 | table td:nth-child(5) { 76 | text-align: center; 77 | } 78 | 79 | #devicelist > ul > li { 80 | padding-top: 10px; 81 | } 82 | 83 | #devicelist > ul > ul > li { 84 | text-decoration: underline; 85 | cursor: default; 86 | } 87 | 88 | li { 89 | list-style-type: none; 90 | font-size: large; 91 | } 92 | 93 | #topbar { 94 | position: sticky; 95 | top: 0; 96 | background-color: blue; 97 | padding: 5px; 98 | font-size: 15px; 99 | color: white; 100 | display: block; 101 | height: 75px; 102 | } 103 | 104 | #container { 105 | display: flex; 106 | flex-direction: row; 107 | height: calc( 100% - 85px) 108 | } 109 | 110 | #devicelist { 111 | padding-right: 10px; 112 | background-color: blue; 113 | color: white; 114 | } 115 | 116 | #devicelist a:visited, 117 | #devicelist a:link, 118 | #devicelist a:hover, 119 | #devicelist a:active { 120 | color: white; 121 | } 122 | 123 | #buildlist { 124 | flex: 1; 125 | width: auto; 126 | } 127 | 128 | #selectdevice { 129 | text-align: center; 130 | padding: 100px; 131 | font-size: x-large; 132 | } -------------------------------------------------------------------------------- /views/ota-list-javascript.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | LineageOTA Builds for {{ branding['name'] }} 4 | 5 | 17 | 18 | 19 |
20 |

Currently available builds for {{ branding['name'] }}

21 |
22 |
23 |
24 |
    25 | {% for vendor,models in devicesByVendor %} 26 |
  • {{ vendor }}
  • 27 | {% for model in models %} 28 |
      29 |
    • {{ model }} ({{ deviceNames[model] }})
    • 30 |
    31 | {% endfor %} 32 | {% endfor %} 33 |
34 |
35 |
36 | 37 |

Please select a device to see the builds that are available.

38 |
39 | {% for model,builds in sortedBuilds %} 40 | 75 | {% endfor %} 76 | 77 |
78 |
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 | --------------------------------------------------------------------------------