diff --git a/.htaccess b/.htaccess index 6e6005b..2464153 100644 --- a/.htaccess +++ b/.htaccess @@ -32,6 +32,10 @@ RewriteRule "^keyboard/(.*)$" "/script/keyboard/keyboard.php?id=$1" [END] RewriteRule "^increment-download/(.*)$" "/script/increment-download/increment-download.php?id=$1" [END] +#### Rewrites for /script folder: /app-downloads-increment + +RewriteRule "^app-downloads-increment/([^/]+)/([^/]+)/(.*)$" "/script/app-downloads-increment/app-downloads-increment.php?product=$1&version=$2&tier=$3" [END] + #### Rewrites for /script folder: /model RewriteRule "^model(/)?$" "/script/model-search/model-search.php" [END] diff --git a/schemas/app-downloads-increment/1.0/app-downloads-increment.json b/schemas/app-downloads-increment/1.0/app-downloads-increment.json new file mode 100644 index 0000000..0ae5fe2 --- /dev/null +++ b/schemas/app-downloads-increment/1.0/app-downloads-increment.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$ref": "#/definitions/app-downloads-increment", + + "definitions": { + "app-downloads-increment": { + "type": "object", + "required": [ + "product", + "version", + "tier", + "count" + ], + "additionalProperties": true, + "properties": { + "product": { "type": "string" }, + "version": { "type": "string" }, + "tier": { "type": "string" }, + "count": { "type": "integer" } + } + } + } +} \ No newline at end of file diff --git a/script/app-downloads-increment/app-downloads-increment.inc.php b/script/app-downloads-increment/app-downloads-increment.inc.php new file mode 100644 index 0000000..887fea6 --- /dev/null +++ b/script/app-downloads-increment/app-downloads-increment.inc.php @@ -0,0 +1,28 @@ +prepare('EXEC sp_app_downloads_increment :product, :version, :tier'); + $stmt->bindParam(":product", $product); + $stmt->bindParam(":version", $version); + $stmt->bindParam(":tier", $tier); + $stmt->execute(); + $data = $stmt->fetchAll(); + if(count($data) == 0) { + return NULL; + } + + $obj = [ + 'product' => $data[0]['product'], + 'version' => $data[0]['version'], + 'tier' => $data[0]['tier'], + 'count' => intval($data[0]['count']) + ]; + + return $obj; + } + } diff --git a/script/app-downloads-increment/app-downloads-increment.php b/script/app-downloads-increment/app-downloads-increment.php new file mode 100644 index 0000000..3e5ff27 --- /dev/null +++ b/script/app-downloads-increment/app-downloads-increment.php @@ -0,0 +1,72 @@ +api_keyman_com .'/schemas/app-downloads-increment/1.0/app-downloads-increment.json#>; rel="describedby"'); + + $AllowGet = isset($_REQUEST['debug']); + + if(!$AllowGet && $_SERVER['REQUEST_METHOD'] != 'POST') { + fail('POST required'); + } + + if(!isset($_REQUEST['key'])) { + fail('key parameter must be set'); + } + + // Note: we don't currently unit-test this one + if(KeymanHosts::Instance()->Tier() === KeymanHosts::TIER_DEVELOPMENT) + $key = 'local'; + else + $key = $env['API_KEYMAN_COM_INCREMENT_DOWNLOAD_KEY']; + + if($_REQUEST['key'] !== $key) { + fail('Invalid key'); + } + + if(!isset($_REQUEST['product']) || + !isset($_REQUEST['version']) || + !isset($_REQUEST['tier']) + ) { + // We don't constrain what the product / version / tier may be here, because + // we may add other products in the future + fail('product, version, tier parameters must be set'); + } + + $product = $_REQUEST['product']; + $version = $_REQUEST['version']; + $tier = $_REQUEST['tier']; + + /** + * POST https://api.keyman.com/app-downloads-increment/product/version/tier + * + * Increments the download counter for a single product identified by + * `product`, `version`, and `tier`. Returns the new total count for the + * product/version/tier for the day + * + * https://api.keyman.com/schemas/app-downloads-increment.json is JSON schema + * + * @param product the name of the product to increment ( "android", "ios", + * "linux", "macos", "web", "windows", "developer"...) + * @param version the version number ("1.2.3") + * @param tier the tier of the product ("alpha", "beta", "stable") + * @param key internal key to allow endpoint to run + */ + + $json = \Keyman\Site\com\keyman\api\AppDownloads::increment($mssql, $product, $version, $tier); + if($json === NULL) { + fail("Failed to increment stat, invalid parameters [$product, $version, $tier]?", 401); + } + + echo json_encode($json, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); \ No newline at end of file diff --git a/tools/db/build/search.sql b/tools/db/build/search.sql index 547e929..d3aaf68 100644 --- a/tools/db/build/search.sql +++ b/tools/db/build/search.sql @@ -306,7 +306,10 @@ CREATE TABLE t_dbdatasources ( DROP TABLE IF EXISTS t_keyboard_downloads; GO ---add a new schema for kstats here so we can use it in search.sql +-- tables in the kstats schema are persistent on the production +-- infrastructure, unlike the other tables in the database, which +-- are recreated on each deployment. + IF SCHEMA_ID('kstats') IS NULL BEGIN EXEC sp_executesql N'CREATE SCHEMA kstats' @@ -325,3 +328,18 @@ BEGIN keyboard_id, statdate ) INCLUDE (count) END + +IF OBJECT_ID('kstats.t_app_downloads', 'U') IS NULL +BEGIN + CREATE TABLE kstats.t_app_downloads ( + product NVARCHAR(64) NOT NULL, -- "android", "ios", "linux", "macos", "web", "windows", "developer"... + version NVARCHAR(64) NOT NULL, -- "123.456.789" + tier NVARCHAR(16) NOT NULL, -- "alpha", "beta", "stable" + statdate DATE, + count INT NOT NULL + ) + + CREATE INDEX ix_app_downloads ON kstats.t_app_downloads ( + product, version, tier, statdate + ) INCLUDE (count) +END diff --git a/tools/db/build/sp_app_downloads_increment.sql b/tools/db/build/sp_app_downloads_increment.sql new file mode 100644 index 0000000..8b7ea71 --- /dev/null +++ b/tools/db/build/sp_app_downloads_increment.sql @@ -0,0 +1,39 @@ +/* + sp_app_downloads_increment +*/ + +DROP PROCEDURE IF EXISTS sp_app_downloads_increment; +GO + +CREATE PROCEDURE sp_app_downloads_increment ( + @prmProduct NVARCHAR(64), + @prmVersion NVARCHAR(64), + @prmTier NVARCHAR(16) +) AS +BEGIN + SET NOCOUNT ON; + + BEGIN TRANSACTION; + + DECLARE @date DATE, @count INT; + + SET @date = CONVERT(date, GETDATE()); + + UPDATE kstats.t_app_downloads + WITH (UPDLOCK, SERIALIZABLE) -- ensure that this statement is atomic with following INSERT + SET count = count + 1, @count = count + 1 + WHERE product = @prmProduct AND version = @prmVersion AND tier = @prmTier AND statdate = @date; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT kstats.t_app_downloads (product, version, tier, statdate, count) + SELECT @prmProduct, @prmVersion, @prmTier, @date, 1 + SET @count = 1 + END + + SET NOCOUNT OFF + + SELECT @prmProduct product, @prmVersion version, @prmTier tier, @count count + + COMMIT TRANSACTION; +END diff --git a/tools/db/build/sp_increment_download.sql b/tools/db/build/sp_increment_download.sql index c628bcd..2f6f6c8 100644 --- a/tools/db/build/sp_increment_download.sql +++ b/tools/db/build/sp_increment_download.sql @@ -1,5 +1,6 @@ /* sp_increment_download + TODO: rename to sp_keyboard_downloads_increment */ DROP PROCEDURE IF EXISTS sp_increment_download;