Improve [F-droid] support to .yml metadata format (#2377)

closes: #2083
This commit is contained in:
Camilo QS
2018-12-07 05:45:19 +00:00
committed by Paul Melnikow
parent d0c9da03c8
commit 5b5ec38337
2 changed files with 243 additions and 73 deletions

View File

@@ -1,20 +1,78 @@
'use strict'
const Joi = require('joi')
const yaml = require('js-yaml')
const BaseService = require('../base')
const { addv: versionText } = require('../../lib/text-formatters')
const { version: versionColor } = require('../../lib/color-formatters')
const { InvalidResponse } = require('../errors')
module.exports = class FDroid extends BaseService {
async fetch({ appId }) {
// currently, we only use the txt format. There are few apps using the yml format.
const url = `https://gitlab.com/fdroid/fdroiddata/raw/master/metadata/${appId}.txt`
const { buffer } = await this._request({
url,
static render({ version }) {
return {
message: versionText(version),
color: versionColor(version),
}
}
async handle({ appId }, queryParams) {
const constructor = this.constructor
const { metadata_format: format } = constructor.validateParams(queryParams)
const url = `https://gitlab.com/fdroid/fdroiddata/raw/master/metadata/${appId}`
const fetchOpts = {
options: {},
errorMessages: {
404: 'app not found',
},
}
const fetch = format === 'yml' ? this.fetchYaml : this.fetchText
let result
try {
// currently, we only use the txt format to the initial fetch because
// there are more apps with that format but yml is now the standard format
// on f-droid, so if txt is not found we look for yml as the fallback
result = await fetch.call(this, url, fetchOpts)
} catch (error) {
if (format) {
// if the format was specified it doesn't make the fallback request
throw error
}
result = await this.fetchYaml(url, fetchOpts)
}
return constructor.render(result)
}
async fetchYaml(url, options) {
const { buffer } = await this._request({
url: `${url}.yml`,
...options,
})
// we assume the yaml layout as provided here:
// https://gitlab.com/fdroid/fdroiddata/raw/master/metadata/org.dystopia.email.yml
try {
const { CurrentVersion: version } = yaml.safeLoad(
buffer.toString(),
'utf8'
)
if (!version) {
throw new Error('could not find version on website')
}
return { version }
} catch (error) {
throw new InvalidResponse({
prettyMessage: 'invalid response',
underlyingError: error,
})
}
}
async fetchText(url, options) {
const { buffer } = await this._request({
url: `${url}.txt`,
...options,
})
const metadata = buffer.toString()
// we assume the layout as provided here:
@@ -25,6 +83,7 @@ module.exports = class FDroid extends BaseService {
const lastVersion = metadata.substring(
positionOfCurrentVersionAtEndOfTheFile
)
const match = lastVersion.match(/^Current Version:\s*(.*?)\s*$/m)
if (!match) {
throw new InvalidResponse({
@@ -35,16 +94,12 @@ module.exports = class FDroid extends BaseService {
return { version: match[1] }
}
static render({ version }) {
return {
message: versionText(version),
color: versionColor(version),
}
}
static validateParams(queryParams) {
const queryParamsSchema = Joi.object({
metadata_format: Joi.string().valid(['yml', 'txt']),
}).required()
async handle({ appId }) {
const result = await this.fetch({ appId })
return this.constructor.render(result)
return this._validateQueryParams(queryParams, queryParamsSchema)
}
// Metadata
@@ -61,6 +116,7 @@ module.exports = class FDroid extends BaseService {
base: 'f-droid/v',
format: '(.+)',
capture: ['appId'],
queryParams: ['metadata_format'],
}
}
@@ -73,6 +129,14 @@ module.exports = class FDroid extends BaseService {
staticExample: this.render({ version: '1.0' }),
keywords: ['fdroid', 'android', 'app'],
},
{
title: 'F-Droid (explicit metadata format)',
exampleUrl: 'org.dystopia.email',
pattern: ':appId',
queryParams: { metadata_format: 'yml' },
staticExample: this.render({ version: '1.2.1' }),
keywords: ['fdroid', 'android', 'app'],
},
]
}
}

View File

@@ -6,87 +6,193 @@ const t = new ServiceTester({ id: 'f-droid', title: 'F-Droid' })
const Joi = require('joi')
module.exports = t
const testString =
'Categories:System\n' +
'License:MIT\n' +
'Web Site:https://github.com/axxapy/apkExtractor/blob/HEAD/README.md\n' +
'Source Code:https://github.com/axxapy/apkExtractor\n' +
'Issue Tracker:https://github.com/axxapy/apkExtractor/issues\n' +
'\n' +
'Auto Name:Apk Extractor\n' +
'Summary:Get APK files from installed apps\n' +
'Description:\n' +
'Extract APKs from your device, even if installed from the Playstore. Root access\n' +
'is required for paid apps.\n' +
'\n' +
'* Fast and easy to use.\n' +
'* Extracts almost all applications, includes system applications.\n' +
'* ROOT access only required for extracting paid apps.\n' +
"* Apk's will be saved in /sdcard/Download/Eimon/.\n" +
'* Provided Search option to search applications.\n' +
'* Compatible with latest version of Android 6.0\n' +
'* Saved apk format : AppPackageName.apk.\n' +
'Current Version:1.8\n' +
'.\n' +
'\n' +
'Repo Type:git\n' +
'Repo:https://github.com/axxapy/apkExtractor\n' +
'\n' +
'Build:1.0,1\n' +
' commit=9b3b62c3ceda74b17eaa22c9e4f893aac10c4442\n' +
' gradle=yes\n' +
'\n' +
'Build:1.1,2\n' +
' commit=1.1\n' +
' gradle=yes\n' +
'\n' +
'Build:1.2,3\n' +
' disable=lintVitalRelease fails\n' +
' commit=1.2\n' +
' gradle=yes\n' +
'\n' +
'Build:1.3,4\n' +
' commit=1.3\n' +
' gradle=yes\n' +
'\n' +
'Build:1.4,5\n' +
' commit=1.4\n' +
' gradle=yes\n' +
'\n' +
'Auto Update Mode:Version %v\n' +
'Update Check Mode:Tags\n' +
'Current Version:1.4\n' +
'Current Version Code:5\n'
const base = 'https://gitlab.com'
const path = '/fdroid/fdroiddata/raw/master/metadata/axp.tool.apkextractor.txt'
const testString = `
Categories:System
License:MIT
Web Site:https://github.com/axxapy/apkExtractor/blob/HEAD/README.md
Source Code:https://github.com/axxapy/apkExtractor
Issue Tracker:https://github.com/axxapy/apkExtractor/issues
t.create('Package is found')
Auto Name:Apk Extractor
Summary:Get APK files from installed apps
Description:
Extract APKs from your device, even if installed from the Playstore. Root access
is required for paid apps.
* Fast and easy to use.
* Extracts almost all applications, includes system applications.
* ROOT access only required for extracting paid apps.
* Apks will be saved in /sdcard/Download/Eimon/.
* Provided Search option to search applications.
* Compatible with latest version of Android 6.0
* Saved apk format : AppPackageName.apk.
Current Version:1.8
Repo Type:git
Repo:https://github.com/axxapy/apkExtractor
Build:1.0,1
commit=9b3b62c3ceda74b17eaa22c9e4f893aac10c4442
gradle=yes
Build:1.1,2
commit=1.1
gradle=yes
Build:1.2,3
disable=lintVitalRelease fails
commit=1.2
gradle=yes
Build:1.3,4
commit=1.3
gradle=yes
Build:1.4,5
commit=1.4
gradle=yes
Auto Update Mode:Version %v
Update Check Mode:Tags
Current Version:1.4
Current Version Code:5
`
const testYmlString = `
Categories: System
License: MIT
WebSite: https://github.com/axxapy/apkExtractor/blob/HEAD/README.md
SourceCode: https://github.com/axxapy/apkExtractor
IssueTracker: https://github.com/axxapy/apkExtractor/issues
AutoName: Apk Extractor
Summary: Get APK files from installed apps
Description: |-
Extract APKs from your device, even if installed from the Playstore. Root access
is required for paid apps.
* Fast and easy to use.
* Extracts almost all applications, includes system applications.
* ROOT access only required for extracting paid apps.
* Apk's will be saved in /sdcard/Download/Eimon/.
* Provided Search option to search applications.
* Compatible with latest version of Android 6.0
* Saved apk format : AppPackageName.apk.
RepoType: git
Repo: https://github.com/axxapy/apkExtractor
Builds:
- versionName: '1.2'
versionCode: 32
commit: '0.32'
subdir: app
gradle:
- yes
- versionName: '1.4'
versionCode: 33
commit: '5'
subdir: app
gradle:
- yes
AutoUpdateMode: Version %v
UpdateCheckMode: Tags
CurrentVersion: 1.4
CurrentVersionCode: 33
`
const base = 'https://gitlab.com'
const path = '/fdroid/fdroiddata/raw/master/metadata/axp.tool.apkextractor'
t.create('Package is found with default metadata format')
.get('/v/axp.tool.apkextractor.json')
.intercept(nock =>
nock(base)
.get(path)
.get(`${path}.txt`)
.reply(200, testString)
)
.expectJSON({ name: 'f-droid', value: 'v1.4' })
t.create('Package is found with fallback yml matadata format')
.get('/v/axp.tool.apkextractor.json')
.intercept(nock =>
nock(base)
.get(`${path}.txt`)
.reply(404)
)
.intercept(nock =>
nock(base)
.get(`${path}.yml`)
.reply(200, testYmlString)
)
.expectJSON({ name: 'f-droid', value: 'v1.4' })
t.create('Package is found with yml matadata format')
.get('/v/axp.tool.apkextractor.json?metadata_format=yml')
.intercept(nock =>
nock(base)
.get(`${path}.yml`)
.reply(200, testYmlString)
)
.expectJSON({ name: 'f-droid', value: 'v1.4' })
t.create('Package is not found with "metadata_format" query parameter')
.get('/v/axp.tool.apkextractor.json?metadata_format=yml')
.intercept(nock =>
nock(base)
.get(`${path}.yml`)
.reply(404)
)
.expectJSON({ name: 'f-droid', value: 'app not found' })
t.create('Package is found yml matadata format with missing "CurrentVersion"')
.get('/v/axp.tool.apkextractor.json?metadata_format=yml')
.intercept(nock =>
nock(base)
.get(`${path}.yml`)
.reply(200, 'Categories: System')
)
.expectJSON({ name: 'f-droid', value: 'invalid response' })
t.create('Package is found with bad yml matadata format')
.get('/v/axp.tool.apkextractor.json?metadata_format=yml')
.intercept(nock =>
nock(base)
.get(`${path}.yml`)
.reply(200, '.CurrentVersion: 1.4')
)
.expectJSON({ name: 'f-droid', value: 'invalid response' })
t.create('Package is not found')
.get('/v/axp.tool.apkextractor.json')
.intercept(nock =>
nock(base)
.get(path)
.reply(404, testString)
.get(`${path}.txt`)
.reply(404)
)
.intercept(nock =>
nock(base)
.get(`${path}.yml`)
.reply(404)
)
.expectJSON({ name: 'f-droid', value: 'app not found' })
t.create('The api changed')
.get('/v/axp.tool.apkextractor.json')
.get('/v/axp.tool.apkextractor.json?metadata_format=yml')
.intercept(nock =>
nock(base)
.get(path)
.get(`${path}.yml`)
.reply(200, '')
)
.expectJSON({ name: 'f-droid', value: 'invalid response' })
t.create('Package is not found due invalid metadata format')
.get('/v/axp.tool.apkextractor.json?metadata_format=xml')
.expectJSON({
name: 'f-droid',
value: 'invalid query parameter: metadata_format',
})
/* If this test fails, either the API has changed or the app was deleted. */
t.create('The real api did not change')
.get('/v/org.thosp.yourlocalweather.json')