'synthetic-1250000000', 'region' => 'ap-guangzhou']; public array $objects = []; public array $multipart = []; public array $calls = []; public int $losePartAck = 0; public int $loseFinishAck = 0; public int $failHead = 0; public int $denyHead = 0; public int $failDelete = 0; public int $loseCreateAck = 0; public int $partWrites = 0; public int $finishes = 0; public array $deletedVersions = []; public $onPart = null; public function location(): array { return $this->current; } public function key(array $o): string { return $o['bucket'] . '|' . $o['region'] . '|' . $o['object_key']; } public function initiate(array $o): string { $id = 'fake-' . (count($this->multipart) + 1); $this->multipart[$id] = ['object' => $o, 'parts' => []]; $this->calls[] = 'initiate'; if ($this->loseCreateAck-- > 0) { throw new RuntimeException('synthetic create acknowledgement lost'); } return $id; } public function part(array $o, int $number, string $cipher): string { if (!isset($this->multipart[$o['multipart_id']])) { throw new RuntimeException('synthetic session missing'); } $this->multipart[$o['multipart_id']]['parts'][$number] = $cipher; $this->partWrites++; $this->calls[] = 'part'; if ($this->onPart !== null) { $callback = $this->onPart; $this->onPart = null; $callback(); } if ($this->losePartAck-- > 0) { throw new RuntimeException('synthetic part acknowledgement lost'); } return md5($cipher); } public function finish(array $o, array $parts): void { $session = $this->multipart[$o['multipart_id']] ?? null; if (!$session) { throw new RuntimeException('synthetic multipart absent'); } $bytes = ''; foreach ($parts as $part) { $body = $session['parts'][$part['PartNumber']] ?? ''; if (md5($body) !== $part['ETag']) { throw new RuntimeException('synthetic part mismatch'); } $bytes .= $body; } $this->objects[$this->key($o)] = ['bytes' => $bytes, 'context' => $o['context_hash'], 'version_id' => 'version-' . (++$this->finishes)]; unset($this->multipart[$o['multipart_id']]); $this->calls[] = 'finish'; if ($this->loseFinishAck-- > 0) { throw new RuntimeException('synthetic finish acknowledgement lost'); } } public function head(array $o, bool $pinned = false): ?array { $this->calls[] = 'head'; if ($this->denyHead-- > 0) { throw new DomainException('云端存储授权不可用,请联系管理员检查配置'); } if ($this->failHead-- > 0) { throw new RuntimeException('synthetic COS down'); } $object = $this->objects[$this->key($o)] ?? null; if (!$object || ($pinned && $o['version_id'] !== '' && $o['version_id'] !== $object['version_id'])) { return null; } return ['size' => strlen($object['bytes']), 'context' => $object['context'], 'version_id' => $object['version_id']]; } public function range(array $o, int $start, int $end): string { $object = $this->objects[$this->key($o)] ?? null; if (!$object || $o['version_id'] !== $object['version_id']) { throw new RuntimeException('synthetic pinned object absent'); } if ($end - $start + 1 > 1048592) { throw new RuntimeException('unbounded cloud read'); } $this->calls[] = 'range:' . $start . '-' . $end; return substr($object['bytes'], $start, $end - $start + 1); } public function delete(array $o): void { if ($this->failDelete-- > 0) { throw new RuntimeException('synthetic delete failed'); } $object = $this->objects[$this->key($o)] ?? null; if ($object && $object['version_id'] !== $o['version_id']) { throw new RuntimeException('unversioned delete forbidden in fixture'); } $this->deletedVersions[] = $o['version_id']; unset($this->objects[$this->key($o)]); $this->calls[] = 'delete'; } public function abort(array $o): void { unset($this->multipart[$o['multipart_id']]); $this->calls[] = 'abort'; } }