php/googledrive.php
<?php
/* Google Drive Interface
docs + links
api: https://developers.google.com/drive/api/guides/about-sdk
file fields: https://developers.google.com/drive/api/reference/rest/v3/files
guide https://developers.google.com/drive/api/guides/manage-uploads examples in php http etc.
authorize https://developers.google.com/identity/protocols/oauth2/web-server
----------- installation
download latest release, select zip with appropriate release and expand
https://github.com/googleapis/google-api-php-client/releases
expand and remove in vendor/google/apiclient-services/src everything except Drive and Drive.php
https://github.com/googleapis/google-api-php-client-services/releases
*/
require_once 'net.php';
class GdRowPath extends RowWorker {
public $paPa, $ppl;
public function config($ppl, $next) {
parent::config($ppl, $next);
$this->ppl = $ppl;
}
public function begin() { $this->paPa = $this->ppl->ppc->paPa; }
public function end() { }
public function write($row) {
$p = $this->ppl->ppc;
if (array_key_exists('path', $row))
$row['path'] = $paN = Cloud::pathCat($this->paPa ??= $p->cloud->pathOf($row['parent']), $row['name'], $row['type']);
$id = (($this->ppl->doBits & Net::RECUR) and $row['type'] === Cloud::DIR) ? $row['id'] : false;
foreach ($p->rwcR2W as $k => $v)
unset($row[$k]);
$this->writerNext->write($row);
if ($id) {
$paO = $this->paPa;
isset($paN) and $this->paPa = $paN;
$p->cloud->nextPageLoop($this->ppl, $id, $p->rwaQry);
$this->paPa = $paO;
}
}
} # end class GdRowPath;
GoogleDrive::ini();
class GoogleDrive {
/*
docs: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/index.html
*/
public const MTDIR = 'application/vnd.google-apps.folder'
, TYPEG2C = [self::MTDIR => CLOUD::DIR]
, URIFILES = 'https://www.googleapis.com/drive/v3/files'
, URIUPLOAD = 'https://www.googleapis.com/upload/drive/v3/files'
, URIAUTH = 'https://oauth2.googleapis.com/token'
;
public static $TYPEC2G;
public static $rowCfg = [
# 0=>google field, 1=>row key,2=>fmtMethBegin, 3=>fmtMethEnd, 4 => fldname in PathParm
[ ['nextPageToken', '', 'aPpl', 0, 0]
, ['name', 'name', 'aText']
, ['mimeType', 'type','aType']
, ['id', 'id', 'aText']
, ['modifiedTime', 'modTst', 'aTst']
, ['size', 'size', 'aText']
, ['owners', 'owner', 'ownerB', '', 'owners(emailAddress)']
, [ 0, 'path', '', 'hrefE', '']
, ['parents', 'parent', 'parentB']
, ['createdTime', 'creTst', ' aTst ']
, ['version', 'version', 'aText']
, ['md5Checksum', 'md5Checksum', 'aText']
, ['sha1Checksum', 'sha1Checksum', 'aText']
, ['sha256Checksum', 'sha256Checksum', 'aText']
], ['path' => 1, 'creTst' => 1]]
;
public static function ini() {
self::$TYPEC2G = array_flip(self::TYPEG2C);
Pipeline::cfgBase(self::$rowCfg);
self::$rowCfg['%path'] = 'path type name id parent';
}
public static function RowCfg($flds) {
($flds) or $flds='%';
if (is_string($flds) and $flds[0] === '%')
return isset(($c = self::$rowCfg[$flds] ?? err("rowCfg $flds does not exist in", array_keys(self::$rowCfg)))['rwEm'])
? $c : self::$rowCfg[$flds] = self::rowCfg($c);
$rwc = Pipeline::rowCfg(self::$rowCfg, $flds);
$rwc['gdFlds'] = implode(',', $rwc[4]);
return $rwc;
}
public $meta, $pathOfPpl, $cAuth, $hAuth, $accessTk, $fun;
public function __construct(public $clc) { } # clc: cloud configuration
public function pipeline($rwc=null, $trg = null, $doBits = Net::PHST, $out = null) {
if (is_string($rwc) and ! $trg)
list($rwc, $trg, $doBi) = explode(',', trim($rwc) . ",,");
if (! isset($rwc['rwEm']))
$rwc = self::rowCfg($rwc);
$trg or $trg = 'metaRowOut';
$p = ($pipe = new Pipeline($this, ('' !== ($doBi ?? '') ? $doBi : $doBits), $rwc))->ppc;
$out and $p->out = $out;
$p->req = new Net2Str;
$p->httpChk = 'HTTP/1.1 200 OK';
if ($trg === 'string')
return $pipe;
$p->json = $trg !== 'jsonObj';
if (str_starts_with($trg, 'json'))
return $pipe;
$p->walk = new WalkJson(); #, allB: 'outB', allE: 'outE');
$p->walk->typeMap = self::TYPEG2C;
return $pipe->makeWalk($trg);
}
/*----- pplCfg: configure ppl for GdRowPath for rowWrite2
a GdRowPath is needed, if subdirectories should be visited recursively, or if we need to compute the path
if ppl is a string create pipeline, if it needs a change in doBits (by & $mask), clone it
add or remove GdRowPath in rowWrite2 as needed by RECUR or path
-----*/
public function pplCfg($ppl, $doBits = Net::PHST, $mask = ~0, $rwPa = 0, $paPa=null) {
is_object($ppl) ? ($rwPa & 2 or $ppl->doBits & ~$mask and $ppl = new Pipeline($ppl, doBits: $ppl->doBits & $mask)) : ($ppl = $this->pipeline($ppl, doBits: $doBits)) and $ppl->doBits &= $mask;
$kyA = ($p = $ppl->ppc)->rwcR['rwEm'];
$doB = $ppl->doBits;
$p->paPa = $paPa;
if (! ($rwPa & 1 and ($doB & Net::RECUR or isset($kyA['path'])))) { # no worker needed
if ($p->rowWrite2) {
$p->rowWriter = $p->rowWrite2;
$p->rowWrite2 = null;
$p->rwcW = $p->rwcR;
}
return $ppl;
}
$kyN = $ppl->doBits & Net::RECUR ? ['id' => 1, 'type' => 1] : [];
if (isset($kyA['path'])) {
$kyN['type'] = $kyN['name'] = 1;
$paPa or $kyN['parent'] = 1;
}
foreach ($kyN as $k => $v)
if (isset($kyA[$k]))
unset($kyN[$k]);
else
$kyA[$k] = 1;
$p->rowWrite2 ??= $p->rowWriter;
$p->rowWriter instanceof GdRowPath or $p->rowWriter = new GdRowPath;
$p->rwcW = ($p->rwcR2W = $kyN) ? self::rowCfg(array_keys($kyA)) : $p->rwcR;
return $ppl;
}
/*----- set accessToken and related fields -----*/
public function accessToken($at) {
$this->accessTk = $at;
$this->hAuth = "Authorization: Bearer " . $this->accessTk['access_token'];
dbg1("auth $this->hAuth, accessToken", $this->accessTk);
$this->cAuth = stream_context_create(['http' => ['header' => $this->hAuth]]);
#. "\nAccept: application/json" # . ' realm="https://www.googleapis.com/auth/documents.readonly"' # . $this->accessTk['scope'] . '"'
$r = $this->get('id owner,Row1', 'root');
if ($r['owner'] !== $this->clc->user)
err("mismatch GoogleDrive(user {$this->clc->user}) but root user $r[1]:", $r);
}
/*----- authorize this for
cliN = client name, i.e. the application/project as register at google a
at Konto ...Drittanbieter-Apps und -Dienste: https://myaccount.google.com/connections
rorId = resource owner Name, i.e.the user owning the drive
-----*/
public function authorize() {
$clc = $this->clc;
if ($oldToken = $clc->tokenGet()) { // get previously obtained accessToken
$expIn = $oldToken['created']+$oldToken['expires_in'] - time();
dbg1("authorize expIn $expIn");
if ($expIn > 1450) # token is not expired not does expire within the next minute ==> use it
return $this->accessToken($oldToken);
out("resource owner $clc->cliN access token expired or expires in $expIn seconds - refreshing");
$cw = ($ci = $clc->clientInfo())['web'];
dbg1("cw", $cw, 'ci', $ci);
$ctx = stream_context_create(['http' => $tx = ['method' => 'POST' , 'header' => 'Content-Type: application/x-www-form-urlencoded'
, 'content' => "client_id=$cw[client_id]&client_secret=$cw[client_secret]&grant_type=refresh_token&refresh_token=$oldToken[refresh_token]"]]);
dbg1("refresh req", $cw['token_uri'], $tx);
$tst = time();
$newToken = $this->pipeline(',jsonArr')->request($cw['token_uri'], $ctx);
dbg1("refresh", $newToken);
$newToken['refresh_token'] ??= $oldToken['refresh_token'] ?? err("no refresh_token", $oldToken);
if ( isset($newToken['access_token']))
out("refreshed ok");
else
unset($newToken);
}
if (! isset($newToken)) {
// Request authorization from the user using google authorization.
$cw ??= ($ci = $clc->clientInfo())['web'];
$state = base64_encode(random_bytes(16));
$authUrl = $cw['auth_uri'] # ??? https://accounts.google.com/o/oauth2/v2/auth
. '?response_type=code&access_type=offline&prompt=consent'
. "&client_id=$cw[client_id]"
. '&redirect_uri=' . $clc->fact->redirUri
. "&state=$state"
. "&scope=" . implode(' ', $clc->scopes);
# dbQ("--- authUrl", $authUrl);
$code = $clc->codeGet($authUrl, $state);
out("--- got code", $code);
// Exchange authorization code for an access token.
$ctx = stream_context_create(['http' => $tx = ['method' => 'POST' , 'header' => 'Content-Type: application/x-www-form-urlencoded'
, 'content' => "client_id=$cw[client_id]&client_secret=$cw[client_secret]&grant_type=authorization_code&code=$code&redirect_uri={$clc->fact->redirUri}"]]);
# dbQ("exchange req", $cw['token_uri'], $tx);
$tst = time();
$newToken = $this->pipeline(',jsonArr')->request($cw['token_uri'], $ctx);
# dbQ("exchanged", $newToken);
out("got new resource owner access token", $newToken);
}
if (! isset($newToken['access_token']))
err("accesstoken invalid ", $newToken);
$newToken["created"] ??= $tst;
$clc->tokenPut($newToken);
return $this->accessToken($newToken);
}
/*----- authorize using php api lib -----*/
public function authorizeApiLib() {
require_once 'googleClientLib/google-api-small/vendor/autoload.php';
$clc = $this->clc;
$this->gc = $client = new Google_Client();
# $client->setApplicationName($cliN); # "NQuickstart"); # seems unnecessary ....
$client->setAuthConfig($clc->clientInfo(false));
$client->setScopes($clc->scopes);
$client->setAccessType('offline');
# $client->setDeveloperKey($apikey); # does not work: apiKey only identifies client, and prohibits any access to private data
if ($oldToken = $clc->tokenGet()) { // get previously obtained accessToken
$client->setAccessToken($oldToken);
if (! $client->isAccessTokenExpired())
return $this->accessToken($oldToken);
out("resource owner $clc->rorId access token is expired - refreshing old\n", $oldToken);
// Refresh the token if possible, else fetch a new one.
if ($newToken = $client->fetchAccessTokenWithRefreshToken()) {
if ( ! isset($newToken['access_token']))
unset($newToken);
else
out("refreshed ok", $newToken['refresh_token'] !== $oldToken['refresh_token'] ? "refresh changed\n": "refresh ===\n", $newToken);
}
}
if (! isset($newToken)) {
// Request authorization from the user using google authorization.
$client->setState($state = base64_encode(random_bytes(16)));
$client->setRedirectUri($clc->fact->redirUri);
$client->setPrompt('consent'); # fix 5.12.24 this sends prompt=consent, without it approval_prompt=auto.
# The later gives a shortened authorization screen interaction, but no refresh token is created!
$authUrl = $client->createAuthUrl();
# dbQ('autUrl with prompt=consent', $authUrl, "\nraw decode", rawurldecode($authUrl));
# $authUrl = str_replace('approval_prompt=auto', 'approval_prompt=force', $authUrl);
# $authUrl = str_replace('approval_prompt=auto', 'prompt=consent', $authUrl);
#dbQ('autUrl replaced', $authUrl, "\nraw decode", rawurldecode($authUrl));
$code = $clc->codeGet($authUrl, $state);
// Exchange authorization code for an access token.
out("got code $code");
$newToken = $client->fetchAccessTokenWithAuthCode($code);
out("got newresource owner access token", $newToken);
}
if (! isset($newToken['access_token']))
err("accesstoken invalid ", $newToken);
$clc->tokenPut($newToken);
return $this->accessToken($newToken);
}
/*----- list: get the row for the given id, if $ppl requests path, use the parent path $paPa to avoid the full computation of path -----*/
public function list($ppl, $paId=null, $name=null, $type = null, $q=null, $pageToken=null) {
is_object($ppl) or $ppl = $this->pipeline($ppl);
$uri = self::URIFILES . "?pageSize=200&fields=nextPageToken,files({$ppl->ppc->rwcW['gdFlds']})&orderBy=name" . ($pageToken ? "&pageToken=$pageToken" : '') . '&q=';
$q = 'not trashed' . ($paId ? " and \"$paId\" in parents" : '') . ($name ? " and name=\"$name\"" : '')
. ($type ? (' and mimeType="' . (self::$TYPEC2G[$type] ?? $type) . '"') : '') . ($q ? " and ($q)" : '');
return $ppl->request($uri . rawurlencode($q), $this->cAuth);
}
/*----- meta: return metadata for the current request -----*/
function meta($fun, $dir, $whr=false) {
$now = new DateTime();
return ['timestamp' => toLocalTst($now) , 'rorId' => $this->clc->rorId, 'fun' => $fun, 'dir' => $dir
, 'where' => $whr, 'user'=> $this->clc->user, 'timeZone' => $now->format('T e P')];
}
/*----- get: get the row for the given id, if $ppl requests path, use the parent path $paPa to avoid the full computation of path -----*/
public function get($ppl, $id=null, $path=null, $frDir='root') {
$ppl = $this->pplCfg($ppl, Net::PHST, ~ Net::RECUR, true, $path);
if ($id) {
return $ppl->request(self::URIFILES . "/$id?fields=" . rawurlencode($ppl->ppc->rwcW['gdFlds']), $this->cAuth);
} elseif(! $path) {
return $ppl->request(self::URIFILES . "/$frDir?fields=" . rawurlencode($ppl->ppc->rwcW['gdFlds']), $this->cAuth);
}
#--- follow path to immediate parent}
$frPa = $frDir === 'root' ? '' : (isset($ppl->ppc->rwcR['emRw']['path']) ? $this->pathOf($frDir) : '???');
$c1 = count($pA = explode('/', trim($path, '/')))-1;
try {
$c1 > 0 and $pd = $this->pipeline('id,jsonObj');
for ($id = $frDir, $i=0; $i<$c1; $i++) {
$r = $this->list($pd, paId:$id, name: $pA[$i]);
if (1 !== $fc = count($r->files)) # will be immediatly catched below and path and lastid will be added - could use a goto out of try block to avoid doubble throw
throw $fc < 1 ? new NetNotFound('get by path', rspH: $pd->ppc->req->rspH) : new NetDuplicate("get by path $fc nodes");
$id = $r->files[0]->id ?? err("files[0]->id not in list", $r);
}
$ppl->ppc->paPa = Cloud::pathPar("$frPa$path");
return $this->list($ppl, paId:$id, name: $pA[$i]);
} catch(NetEx $e) { # add path and lastId
throw $e->add(($i < $c1 ? "parent" : "leaf") . " missing in $path", nfPath: array_slice($pA, $i)
, lastPath: $frPa . implode('/', array_slice($pA, 0, $i)) . substr('/', 0, $i>0), lastId: $id);
}
}
/*----- gdCreate: google drive post request to create file/folder, metaData and possibly with content -----*/
public function gdCreate($ppl, $parent, $name, $type, $cnt='', $creTst=null, $modTst=null) {
is_object($ppl) or $ppl = $this->pipeline($ppl);
$type = self::$TYPEC2G[$type] ?? $type;
$ma = ['name' => $name, 'mimeType' => $type, "parents" =>[$parent ?? 'root']];
$creTst and $ma[self::$rowCfg[1]['creTst'][0][0]] = toIsoTst($creTst);
$modTst and $ma[self::$rowCfg[1]['modTst'][0][0]] = toIsoTst($modTst);
$mj = json_encode($ma);
$pp = "fields=" . rawurlencode($ppl->ppc->rwcW['gdFlds']);
if (is_null($cnt) or $cnt === '' ) { # metadata only
$h = ['header' => "Content-Type: application/json; charset=UTF-8\nContent-Length: " . strlen($mj)
, 'content' => $mj];
$uri = self::URIFILES . "?$pp";
} else {
$h = Net::mimeMultipart('application/json; charset=UTF-8', $mj, $type, $cnt);
$uri = self::URIUPLOAD ."?uploadType=multipart&$pp" . ($ppl->doBits & Net::KRF ? '&keepRevisionForever=true' : '');
}
$h['header'] .= "\n$this->hAuth";
$h['method'] = 'post';
dbg1("creating uri $uri, ctx", $h);
return $ppl->request($uri, stream_context_create(['http' => $h]));
}
/*----- pathOf return path of given id -----*/
public function pathOf($id) {
if (! $id)
return Cloud::ROOTPA;
elseif ($id === 'root')
return Cloud::ROOT;
$ppl = $this->pathOfPpl ??= $this->pipeline('name type parent,row1');
$r = $this->get($ppl, $id);
if (! $r['parent'])
return Cloud::ROOT;
$pa = $r['name'] . substr('/', 0, $r['type'] === Cloud::DIR);
while (true) {
$r = $this->get($ppl, $r['parent']);
if (! $r['parent'])
return $pa;
$pa = "$r[name]/$pa";
}
}
/*----- ff find files folders
$dirId, $dirPa id and path of parentfolder, if necessary id is obtained from path, or path from id
$doBits: Cloud:WRIPA write row for parent, Cloud::RECUR recurse depth first into folders (needs dirId or dirPa)
-----*/
public function ff($ppl, $dirId=null, $whr=null, $dirPa = null) {
$ppl = $this->pplCfg($ppl, Net::PHST | Net::RECUR | Net::WRIPA, ~0, 3, true);
$doBits = $ppl->doBits;
$rE = ($p = $ppl->ppc)->rwcR['rwEm'];
$p->cloud === $this or err("ppl->ppc->cloud not this");
!$dirId and !$dirPa and $doBits & Net::RECUR and $dirId = 'root';
isset($p->rowWriter) or $doBits &= ~Net::WRIPA;
if (~$doBits & Net::WRIPA) { #----- do not write dirRow
(! $dirId and $dirPa) and $dirId = $this->get("id,row1", path: $dirPa)['id'];
} elseif ($dirId or $dirPa) { #----- get dirRow from id
$dirRow = $this->get($p->rwcR['rws'] . str_repeat(' id', ! isset($rE['id'])) . str_repeat(' path', ! isset($rE['path'])) . ',row1', id: $dirId, path: $dirPa);
$dirPa = $dirRow['path'];
$dirId = $dirRow['id'];
}
$p->meta = $this->meta('ff', $dirPa ?? '?', $whr);
$ppl->ppc->paPa = $dirPa;
$ppl->setBits($doBits & ~ Net::W1 & ~ Net::END)->request(0, 0);
if (isset($dirRow)) {
foreach ($dirRow as $k => $v)
if (! isset($rE[$k]))
unset($dirRow[$k]);
$p->rowWrite2->write($dirRow);
}
$ppl->setBits($doBits & ~ (Net::CONFIG | Net::BEGIN | Net::END));
isset($p->walk) ? $this->nextPageLoop($ppl, $dirId, $p->rwaQry = $whr) : $this->list($ppl, paId: $dirId, q: $whr);
return $ppl->setBits($doBits & ~ (Net::CONFIG | Net::BEGIN | Net::W1))->request(0, 0);
}
/*----- repeat the request using nextPageToken -----*/
public function nextPageLoop($ppl, $id, $q) {
$nptO = ($p = $ppl->ppc)->nextPageToken ?? false;
$np = null;
do {
$p->nextPageToken = null;
$r = $this->list($ppl, paId: $id, q: $q, pageToken: $np);
} while ($np = $p->nextPageToken);
$p->nextPageToken = $nptO;
}
/*----- create: create file/folder, metaData and possibly with content -----*/
public function create($ppl, $name, $type, $cnt='', $frDir = 'root', $creTst=null, $modTst=null) {
$ppl = $this->pplCfg($ppl, Net::PHST, ~ Net::RECUR, 1, true);
$type = self::$TYPEC2G[$type] ?? $type;
try {
$r = $this->get('id path,Row1', null, $name, $frDir);
throw new NetDuplicate('already exists', id: $r['id'], path: $r['path']);
} catch (NetNotFound $e) {
}
$id = $e->det['lastId'];
if (0 < $lX = count($lA = $e->det['nfPath']) - 1) {
$ppl->doBits & Net::CREPA or throw $e->add("parent missing without CREPA");
$pd = $this->pipeline('id,jsonObj');
for ($i=0 ; $i<$lX; $i++) {
Cloud::fn2valid($lA[$i]) === $lA[$i] or throw NetEx("create bad name $lA[$i] in $name");
$r = $this->gdCreate($pd, parent:$id, name: $lA[$i], type: Cloud::DIR);
$id = $r->id ?? err("->id not in created", $r);
}
}
Cloud::fn2valid($lA[$lX]) === $lA[$lX] or throw NetEx("create bad name $lA[$lX] in $name");
$ppl->ppc->paPa = Cloud::pathPar($e->det['lastPath'] . implode('/', $lA));
$this->fun = 'create';
return $this->gdCreate($ppl, $id, $lA[$lX], $type, $cnt, $creTst, $modTst);
}
/*----- update: update file/folder, metaData, content or both
$kRF: keep Revision forEver
-----*/
public function update($ppl, $id, $type, $cnt='', $path=null, $creTst=null, $modTst=null) { # create file/folder, metaData possibly with content
$ppl = $this->pplCfg($ppl, Net::PHST, ~ Net::RECUR, 1, Cloud::pathPar($path));
$pp = ($fi = "fields=" . rawurlencode($ppl->ppc->rwcW['gdFlds'])) . str_repeat('&keepRevisionForever=true', (bool) ($ppl->doBits & Net::KRF));
$type = self::$TYPEC2G[$type] ?? $type;
$creTst and $ma[self::$rowCfg[1]['creTst'][0][0]] = toIsoTst($creTst);
$modTst and $ma[self::$rowCfg[1]['modTst'][0][0]] = toIsoTst($modTst);
if (! isset($ma)) {
if (is_null($cnt) or $cnt === '' )
netEx("update neither content nor metadata");
$h = ['header' => "Content-Type: $type\nContent-Length: " . strlen($cnt)
, 'content' => $cnt];
$uri = self::URIUPLOAD . "/$id?uploadType=media&$pp";
} else {
$mj = json_encode($ma);
if (is_null($cnt) or $cnt === '' ) { # metadata only
$h = ['header' => "Content-Type: application/json; charset=UTF-8\nContent-Length: " . strlen($mj)
, 'content' => $mj];
$uri = self::URIFILES . "/$id?$fi";
} else { # metadata and content: multipart request
$h = Net::mimeMultipart('application/json; charset=UTF-8', $mj, $type, $cnt);
$uri = self::URIUPLOAD ."/$id?uploadType=multipart&$pp";
}
}
$h['header'] .= "\n$this->hAuth";
$h['method'] = 'patch';
$this->fun = 'update';
return $ppl->request($uri, stream_context_create(['http' => $h]));
}
/*----- if the file at path exists, update it, otherwise create it (after creating necessary folders) -----*/
public function creUpd($ppl, $path, $type, $cnt='', $frDir = 'root', $creTst=null, $modTst=null) { # create file/folder, metaData possibly with content
try {
return $this->create($ppl, $path, $type, $cnt, $frDir, $creTst, $modTst);
} catch (NetDuplicate $e) {
return $this->update($ppl, $e->det['id'], $type, $cnt, $e->det['path'], $creTst, $modTst);
}
/*
str_starts_with($pa = trim($path, '/'), $pE = $e->det['path']) or err ("path $path mismatch catch $pE");
$id=$e->det['lastId'];
$bn = basename($pa);
if (trim($pE) === $pa) { # only basename missing all parents exist
} elseif (~ $ppl->doBits & Net::CREPA) {
throw $e->add("parent missing without CREPA", $ppl->doBits);
} else { # create parents
for ($px = (false === $px = strrpos($pE, '/')) ? 0 : $px+1; $py = strpos($pa, '/', $px); $px = $py+1) {
dbg1("creating dir $px $py:", substr($pa, $px, $py-$px));
$id = $this->create($pi, $id, substr($pa, $px, $py-$px), Cloud::DIR)['id'];
}
}
isset($ppl->ppc->rwcR['rwEm']['path']) and GdRowPath::cfgPpl($ppl, Cloud::pathPar($pa));
out("creating file $bn");
return $this->create($ppl, $id, $bn, $type, $cnt, creTst: $creTst, modTst: $modTst);
}
*/ }
} # end class GoogleDrive