diff --git a/src/data/tuttle-example-config.xml b/src/data/tuttle-example-config.xml index 6124442..25107bb 100644 --- a/src/data/tuttle-example-config.xml +++ b/src/data/tuttle-example-config.xml @@ -64,7 +64,10 @@ - - + + \ No newline at end of file diff --git a/src/modules/api.xql b/src/modules/api.xql index e30b6d3..6a81799 100644 --- a/src/modules/api.xql +++ b/src/modules/api.xql @@ -227,28 +227,25 @@ declare %private function api:pull($config as map(*), $hash as xs:string?) as ma declare function api:git-deploy($request as map(*)) as map(*) { try { let $config := api:get-collection-config($request?parameters?collection) - let $collection-destination := $config?path - let $collection-destination-sha := $config?path || "/gitsha.xml" + let $destination := $config?path let $lockfile := $config?path || "/" || config:lock() - - let $collection-staging := $config?collection || config:suffix() - let $collection-staging-uri := $config?path || config:suffix() + let $staging := $config?path || config:suffix() - let $ensure-destination-collection := collection:create($config?path) + let $ensure-destination-collection := collection:create($destination) return - if (not(xmldb:collection-available($collection-staging-uri))) - then map { "message" : "Staging collection '" || $collection-staging-uri || "' does not exist!" } + if (not(xmldb:collection-available($staging))) + then map { "message" : "Staging collection '" || $staging || "' does not exist!" } else if (doc-available($lockfile)) then map { "message" : doc($lockfile)/task/value/text() || " in progress!" } else if (exists($ensure-destination-collection?error)) then map { "message" : "Could not create destination collection!", "error": $ensure-destination-collection?error } else - let $write-lock := app:lock-write($config?path, "deploy") - let $is-expath-package := xmldb:get-child-resources($collection-staging-uri) = ("expath-pkg.xml", "repo.xml") + let $write-lock := app:lock-write($destination, "deploy") + let $is-expath-package := xmldb:get-child-resources($staging) = ("expath-pkg.xml", "repo.xml") let $deploy := if ($is-expath-package) then ( - let $package := doc(concat($config?path, "/expath-pkg.xml"))//@name/string() + let $package := doc(concat($staging, "/expath-pkg.xml"))//@name/string() let $remove-pkg := if ($package = repo:list()) then ( @@ -260,26 +257,24 @@ declare function api:git-deploy($request as map(*)) as map(*) { let $xar := xmldb:store-as-binary( - $collection-staging-uri, "pkg.xar", - compression:zip(xs:anyURI($collection-staging-uri), true(), $collection-staging-uri)) + $staging, "pkg.xar", + compression:zip(xs:anyURI($staging), true(), $staging)) let $install := repo:install-and-deploy-from-db($xar) return "package installation" ) else ( - let $cleanup-col := app:cleanup-collection($config?collection, config:prefix()) - let $cleanup-res := app:cleanup-resources($config?collection, config:prefix()) - let $move-col := app:move-collections($collection-staging, $config?collection, config:prefix()) - let $move-res := app:move-resources($collection-staging, $config?collection, config:prefix()) - let $set-permissions := app:set-permission($config?path) + let $cleanup-col := app:cleanup-collection($destination) + let $move-col := app:move-collections($staging, $destination) + let $set-permissions := app:set-permission($destination) return "data move" ) - let $remove-staging := collection:remove($collection-staging-uri, true()) - let $remove-lock := app:lock-remove($collection-destination) + let $remove-staging := collection:remove($staging, true()) + let $remove-lock := app:lock-remove($destination) return map { - "hash": config:deployed-sha($config?path), + "hash": config:deployed-sha($destination), "message": "success" } diff --git a/src/modules/app.xql b/src/modules/app.xql index e47735a..7594ed7 100644 --- a/src/modules/app.xql +++ b/src/modules/app.xql @@ -1,112 +1,72 @@ xquery version "3.1"; module namespace app="http://e-editiones.org/tuttle/app"; -declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization"; import module namespace xmldb="http://exist-db.org/xquery/xmldb"; import module namespace http="http://expath.org/ns/http-client"; import module namespace compression="http://exist-db.org/xquery/compression"; import module namespace repo="http://exist-db.org/xquery/repo"; import module namespace sm="http://exist-db.org/xquery/securitymanager"; -import module namespace dbutil="http://exist-db.org/xquery/dbutil"; + +import module namespace collection="http://existsolutions.com/modules/collection" at "collection.xqm"; import module namespace config="http://e-editiones.org/tuttle/config" at "config.xql"; +declare function app:extract-archive($zip as xs:base64Binary, $collection as xs:string) { + compression:unzip($zip, + app:unzip-filter#3, config:ignore(), + app:unzip-store#4, $collection) +}; + (:~ : Unzip helper function :) -declare function app:unzip-store($path as xs:string, $data-type as xs:string, $data as item()?, $param as item()*) { - let $archive-root := substring-before($path, '/') - let $archive-root-length := string-length($archive-root) - let $object := substring-after($path, '/') - - return - if ($data-type = 'folder') then - let $mkcol := app:mkcol($param, $object) - return - - else - let $resource-path := "/" || tokenize($object, '[^/]+$')[1] - let $resource-collection := concat($param, $resource-path) - let $resource-filename := xmldb:encode(replace($object, $resource-path, "")) - return - try { - let $collection-check := if (xmldb:collection-available($resource-collection)) then () else app:mkcol($resource-collection) - let $store := xmldb:store($resource-collection, $resource-filename, $data) - return () - } - catch * { - map { - "_error": map { - "code": $err:code, "description": $err:description, "value": $err:value, - "line": $err:line-number, "column": $err:column-number, "module": $err:module - } - } - } +declare function app:unzip-store($path as xs:string, $data-type as xs:string, $data as item()?, $base as xs:string) as map(*) { + if ($data-type = 'folder') then ( + let $create := collection:create($base || "/" || substring-after($path, '/')) + return map { "path": $path } + ) else ( + try { + let $resource := app:file-to-resource($base, substring-after($path, '/')) + let $collection-check := collection:create($resource?collection) + let $store := xmldb:store($resource?collection, $resource?name, $data) + return map { "path": $path } + } + catch * { + map { "path": $path, "error": $err:description } + } + ) }; (:~ : Filter out ignored resources + : returning true() _will_ extract the file or folder :) -declare function app:unzip-filter($path as xs:string, $data-type as xs:string, $param as item()*) as xs:boolean { - not(contains($path, config:ignore())) +declare function app:unzip-filter($path as xs:string, $data-type as xs:string, $ignore as xs:string*) as xs:boolean { + not(substring-after($path, '/') = $ignore) }; (:~ : Move staging collection to final collection :) -declare function app:move-collections($collection-source as xs:string, $collection-target as xs:string, $prefix as xs:string) { - let $fullpath-collection-source := $prefix || "/" || $collection-source - let $fullpath-collection-target := $prefix || "/" || $collection-target - - return - for $child in xmldb:get-child-collections($fullpath-collection-source) - let $fullpath-child-source := $fullpath-collection-source || "/" || $child - let $fullpath-child-target := $fullpath-collection-target || "/" - return - xmldb:move($fullpath-child-source, $fullpath-child-target) -}; - -(:~ - : Move staging collection to final collection - :) -declare function app:move-resources($collection-source as xs:string, $collection-target as xs:string, $prefix as xs:string) { - let $fullpath-collection-source := $prefix || "/" || $collection-source - let $fullpath-collection-target := $prefix || "/" || $collection-target - - return - for $child in xmldb:get-child-resources($fullpath-collection-source) - return - xmldb:move($fullpath-collection-source, $fullpath-collection-target, $child) +declare function app:move-collections($collection-source as xs:string, $collection-target as xs:string) { + xmldb:get-child-collections($collection-source) + ! xmldb:move($collection-source || "/" || ., $collection-target), + xmldb:get-child-resources($collection-source) + ! xmldb:move($collection-source, $collection-target, .) }; (:~ : Cleanup destination collection - delete collections from target collection :) -declare function app:cleanup-collection($collection as xs:string, $prefix as xs:string) { - let $ignore := [config:ignore(), config:lock()] - let $fullpath-collection := $prefix || "/" || $collection - - return - for $child in xmldb:get-child-collections($fullpath-collection) - where not(contains($child, $ignore)) - let $fullpath-child := $fullpath-collection || "/" || $child - return - xmldb:remove($fullpath-child) -}; - -(:~ - : Cleanup destination collection - delete resources from target collection - :) -declare function app:cleanup-resources($collection as xs:string, $prefix as xs:string) { - let $ignore := config:ignore() - let $fullpath-collection := $prefix || "/" || $collection - - return - for $child in xmldb:get-child-resources($fullpath-collection) - where not(contains($child, $ignore)) - return - xmldb:remove($fullpath-collection, $child) +declare function app:cleanup-collection($collection as xs:string) { + let $ignore := (config:ignore(), config:lock()) + return ( + xmldb:get-child-collections($collection)[not(.= $ignore)] + ! xmldb:remove($collection || "/" || .), + xmldb:get-child-resources($collection)[not(.= $ignore)] + ! xmldb:remove($collection, .) + ) }; (:~ @@ -136,8 +96,8 @@ declare function app:write-apikey($collection as xs:string, $apikey as xs:string try { let $collection-prefix := tokenize(config:apikeys(), '[^/]+$')[1] let $apikey-resource := xmldb:encode(replace(config:apikeys(), $collection-prefix, "")) - let $collection-check := - if (xmldb:collection-available($collection-prefix)) then () else app:mkcol($collection-prefix) + let $collection-check := collection:create($collection-prefix) + return if (doc(config:apikeys())//apikeys/collection[name = $collection]/key/text()) then update replace doc(config:apikeys())//apikeys/collection[name = $collection]/key with {$apikey} @@ -180,79 +140,46 @@ declare function app:lock-remove($collection as xs:string) { }; (:~ -: Set recursive permissions to whole collection -:) + : Set permissions to collection recursively + :) declare function app:set-permission($collection as xs:string) { - let $collection-uri := config:prefix() || "/" || $collection + let $permissions := app:get-permissions($collection) + let $callback := app:set-permission(?, ?, $permissions) - return - dbutil:scan(xs:anyURI($collection-uri), function($collection-url, $resource) { - let $path := ($resource, $collection-url)[1] - return ( - if ($resource) then - app:set-permission($collection, $path, "resource") - else - app:set-permission($collection, $path, "collection") - ) - }) + return + collection:scan($collection, $callback) }; -(:~ -: Set permissions for $path -: $type: 'collection' or 'resource' -:) -declare function app:set-permission($collection as xs:string, $path as xs:string, $type as xs:string) { - let $collection-uri := config:prefix() || "/" || $collection - let $repo := doc(concat($collection-uri, "/repo.xml"))/repo:meta/repo:permissions - - return ( - if (exists($repo)) then ( - sm:chown($path, $repo/@user/string()), - sm:chgrp($path, $repo/@group/string()), - if ($type = "resource") then - sm:chmod($path, $repo/@mode/string()) - else - sm:chmod($path, replace($repo/@mode/string(), "(..).(..).(..).", "$1x$2x$3x")) - ) - else ( - sm:chown($path, config:sm()?user), - sm:chgrp($path, config:sm()?group), - if ($type = "resource") then - sm:chmod($path, config:sm()?mode) - else - sm:chmod($path, replace(config:sm()?mode, "(..).(..).(..).", "$1x$2x$3x"))) - ) +declare function app:get-permissions ($collection as xs:string) { + if (doc-available($collection || "/repo.xml")) then ( + let $repo := doc($collection || "/repo.xml")//repo:permissions + return map { + "user": $repo/@user/string(), + "group": $repo/@group/string(), + "mode": $repo/@mode/string() + } + ) else ( + config:sm() + ) }; (:~ - : Helper function of unzip:mkcol() +: Set permissions for either a collection or resource :) -declare function app:mkcol-recursive($collection, $components) as xs:string* { - if (exists($components)) then - let $newColl := concat($collection, "/", $components[1]) - return ( - xmldb:create-collection($collection, $components[1]), - if ($components[2]) then - app:mkcol-recursive($newColl, subsequence($components, 2)) - else () +declare function app:set-permission($collection as xs:string, $resource as xs:string?, $permissions as map(*)) { + if (exists($resource)) then ( + xs:anyURI($resource) ! ( + sm:chown(., $permissions?user), + sm:chgrp(., $permissions?group), + sm:chmod(., $permissions?mode) ) - else () -}; - -(:~ - : Helper function to recursively create a collection hierarchy - :) -declare function app:mkcol($collection, $path) as xs:string* { - app:mkcol-recursive($collection, - tokenize($path, "/") ! xmldb:encode(.)) -}; - -(:~ - : Helper function to recursively create a collection hierarchy - :) -declare function app:mkcol($path) as xs:string* { - app:mkcol('/db', - substring-after($path, "/db/")) + ) else ( + xs:anyURI($collection) ! ( + sm:chown(., $permissions?user), + sm:chgrp(., $permissions?group), + sm:chmod(., replace($permissions?mode, "(r.)-", "$1x")) + ) + ) }; (:~ @@ -296,10 +223,6 @@ declare function app:request($request as element(http:request)) { else $response }; -declare function app:extract-archive($zip as xs:base64Binary, $collection as xs:string) { - compression:unzip($zip, app:unzip-filter#3, (), app:unzip-store#4, $collection) -}; - (:~ : resolve file path against a base collection : $base the absolute DB path to a collection. Assumes no slash at the end @@ -331,15 +254,15 @@ declare function app:delete-resource($config as map(*), $filepath as xs:string) :) declare function app:add-resource($config as map(*), $filepath as xs:string, $data as item()) as xs:boolean { let $resource := app:file-to-resource($config?path, $filepath) - + let $permissions := app:get-permissions($config?path) let $collection-check := if (xmldb:collection-available($resource?collection)) then () else ( - app:mkcol($resource?collection), - app:set-permission($config?collection, $resource?collection, "collection") + collection:create($resource?collection), + app:set-permission($resource?collection, (), $permissions) ) let $store := xmldb:store($resource?collection, $resource?name, $data) - let $chmod := app:set-permission($config?collection, $resource?collection || $resource?name, "resource") + let $chmod := app:set-permission($resource?collection, $store, $permissions) return true() }; diff --git a/src/modules/collection.xqm b/src/modules/collection.xqm index 9d663ae..dae1027 100644 --- a/src/modules/collection.xqm +++ b/src/modules/collection.xqm @@ -2,6 +2,9 @@ xquery version '3.1'; module namespace collection="http://existsolutions.com/modules/collection"; +import module namespace sm="http://exist-db.org/xquery/securitymanager"; +import module namespace xmldb="http://exist-db.org/xquery/xmldb"; + (:~ : create arbitrarily deep-nested sub-collection : @param $new-collection absolute path that starts with "/db" @@ -68,3 +71,37 @@ declare function collection:remove($path as xs:string, $force as xs:boolean) { then error(xs:QName("collection:not-empty"), "Collection '" || $path || "' is not empty and $force is false().") else xmldb:remove($path) }; + +(:~ + : Scan a collection tree recursively starting at $root. Call the supplied function once for each + : resource encountered. The first parameter to $func is the collection URI, the second the resource + : path (including the collection part). + :) +declare function collection:scan($root as xs:string, $func as function(xs:string, xs:string?) as item()*) { + collection:scan-collection($root, $func) +}; + +(:~ Scan a collection tree recursively starting at $root. Call $func once for each collection found :) +declare %private function collection:scan-collection($collection as xs:string, $func as function(xs:string, xs:string?) as item()*) { + $func($collection, ()), + collection:scan-resources($collection, $func), + for $child-collection in xmldb:get-child-collections($collection) + let $path := concat($collection, "/", $child-collection) + return + if (sm:has-access(xs:anyURI($path), "rx")) then ( + collection:scan-collection($path, $func) + ) else () +}; + +(:~ + : List all resources contained in a collection and call the supplied function once for each + : resource with the complete path to the resource as parameter. + :) +declare %private function collection:scan-resources($collection as xs:string, $func as function(xs:string, xs:string?) as item()*) { + for $child-resource in xmldb:get-child-resources($collection) + let $path := concat($collection, "/", $child-resource) + return + if (sm:has-access(xs:anyURI($path), "r")) then ( + $func($collection, $path) + ) else () +}; diff --git a/test/github.js b/test/github.js index 6635a4e..c637a7b 100644 --- a/test/github.js +++ b/test/github.js @@ -94,7 +94,7 @@ describe('Github', function () { before(async function () { this.timeout(10000); incrementalUpdateResponse = await axios.post('git/incremental', {}, { auth }); - // console.log('incrementalUpdateResponse', incrementalUpdateResponse.data) + // console.log('incrementalUpdateResponse', incrementalUpdateResponse.data.changes) }) it('succeeds', function () {