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