… 结构,只有续拉游标的节点名不同(NextMarker / * NextContinuationToken)。三个驱动共用本 trait,避免同一段 XML 遍历写三遍。 * 使用方需要有 $this->config 以及 ObjectKeyNormalizeTrait 提供的 key 规整方法。 */ trait ObjectListXmlTrait { /** * @param string $xml 响应体 * @param string $urlBase 未配置 domain 时兜底拼地址用的主机前缀(不带尾斜杠) * @param string $tokenNode 续拉游标节点名 * @return array{items: array, next_marker: string, finished: bool} */ protected function parseObjectListXml(string $xml, string $urlBase, string $tokenNode = 'NextMarker'): array { $doc = @simplexml_load_string($xml); if ($doc === false) { UtilsService::getInstance()->errorThrow('列举结果解析失败,返回内容不是合法 XML'); } $items = []; $lastRawKey = ''; $contents = isset($doc->Contents) ? $doc->Contents : []; foreach ($contents as $node) { $rawKey = (string) $node->Key; // 续拉游标必须用服务端原样返回的 key,不能用补过 prefix 的规整值 $lastRawKey = $rawKey; $key = $this->normalizeObjectKey($rawKey); if ($key === '' || str_ends_with($key, '/')) { // 以 / 结尾的是控制台建目录留下的占位对象,不是素材 continue; } $items[] = [ 'key' => $key, 'size' => (int) $node->Size, 'hash' => strtolower(trim((string) $node->ETag, '"')), 'last_modified' => (int) strtotime((string) $node->LastModified), 'url' => $this->publicUrlOf($key, $urlBase . '/' . $key), ]; } $truncated = filter_var((string) ($doc->IsTruncated ?? 'false'), FILTER_VALIDATE_BOOLEAN); $next = isset($doc->{$tokenNode}) ? (string) $doc->{$tokenNode} : ''; if ($truncated && $next === '' && $tokenNode === 'NextMarker') { // 部分兼容实现只给 IsTruncated 不给 NextMarker,按协议可用本页最后一个 key 续拉 $next = $lastRawKey; } return [ 'items' => $items, 'next_marker' => $truncated ? $next : '', 'finished' => !$truncated || $next === '', ]; } }