Add Blob Versioning Support to Azurite - #2735
Add Blob Versioning Support to Azurite#2735Rodolfo Orozco Vasquez (rorozcov) wants to merge 70 commits into
Conversation
…eate blob. No testing yet. All theoretical
…tore. Now local testing needed. Saving progress
…bs and api changes
…tests with versioning enabled. Must add versioning related checks
…Added test to verify
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
This PR adds Azure Blob Storage blob versioning support to Azurite (Loki metadata path), including account-scoped configuration via a new AccountModel persisted in Loki, and updates core blob handlers and listing/pagination behavior to support version-aware operations.
Changes:
- Introduces
AccountModel+LokiAccountModelStoreand wires account configuration into CLI/VS Code environments and server startup. - Adds version-aware behavior across blob operations (create/overwrite, delete, snapshot/copy interactions, list with
includeVersions, andversionIdquery support). - Adds extensive test coverage for versioning behavior and updates docs/README and VS Code extension settings.
Reviewed changes
Copilot reviewed 51 out of 53 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/testutils.ts | Adds test helpers for versioning scenarios (context/container/blob builders, version listing helper). |
| tests/common/LokiAccountModelStore.test.ts | New unit tests validating account model persistence/merge semantics. |
| tests/BlobTestServerFactory.ts | Allows injecting/creating an account model store for test servers. |
| tests/blob/utils.test.ts | Adds tests for new utility helpers (isNullOrWhitespace, parseDateFromAssumedString). |
| tests/blob/upgradeRegression.test.ts | Updates regression tests to include account model store in configuration. |
| tests/blob/startupErrorRecovery.test.ts | Updates startup recovery tests to include account model store in configuration. |
| tests/blob/pagewithdelimiter.test.ts | Updates pagination marker tests for version-aware marker behavior and adds versioning cases. |
| tests/blob/lokidb.test.ts | Adds Loki metadata store tests for versioning-disabled behavior and transitions. |
| tests/blob/handlers/AppendBlobHandler.test.ts | Updates handler mocks/signatures impacted by version-aware metadata store APIs. |
| tests/blob/apis/versioning.production.parity.test.ts | Adds skipped/manual production parity tests for versioning transitions and edge cases. |
| tests/blob/apis/versioning.azurite.parity.test.ts | Adds Azurite parity tests for versioning transitions and invalid combinations. |
| tests/blob/apis/pageblob.versioning.test.ts | Adds page blob API tests for version creation, access, listing, deletion, and validation. |
| tests/blob/apis/pageblob.test.ts | Adds assertion that versionId remains undefined when versioning is disabled. |
| tests/blob/apis/container.test.ts | Updates continuation-token assertions and adds list behavior checks for version fields when not requested. |
| tests/blob/apis/blockblob.versioning.test.ts | Adds block blob API tests for version creation, access, listing, deletion, and validation. |
| tests/blob/apis/blockblob.test.ts | Adds assertion that versionId remains undefined when versioning is disabled (commitBlockList). |
| tests/blob/apis/appendblob.versioning.test.ts | Adds append blob API tests for version creation, access, listing, deletion, and validation. |
| tests/blob/apis/appendblob.test.ts | Adds assertion that versionId remains undefined when versioning is disabled. |
| src/common/VSCServerManagerBlob.ts | Switches VS Code startup to use BlobServerFactory and initializes account model store. |
| src/common/VSCEnvironment.ts | Adds VS Code settings parsing for account models via parseAccountModelFlags. |
| src/common/IEnvironment.ts | Extends environment contract to include account model configuration. |
| src/common/IAccountModelEnvironment.ts | New interface for environments exposing account model configuration. |
| src/common/EnvironmentFunctions.ts | Adds parsing logic for file/JSON account model configurations (single + multi-account). |
| src/common/Environment.ts | Adds CLI flags and environment integration for account model configuration. |
| src/common/ConfigurationBase.ts | Minor formatting/cleanup. |
| src/common/account/LokiAccountModelStore.ts | New Loki-backed account model store with persistence and merge behavior. |
| src/common/account/index.ts | Exports account store module entry point. |
| src/blob/utils/utils.ts | Adds date parsing helper for validating versionId/snapshot formats and isNullOrWhitespace. |
| src/blob/utils/constants.ts | Adds default Loki DB path for account models. |
| src/blob/persistence/SqlBlobMetadataStore.ts | Updates interfaces/signatures for versioning-related parameters (still not supporting versioning). |
| src/blob/persistence/PageWithDelimiter.ts | Updates paging markers to support (name,timestamp) ordering for version-aware continuation. |
| src/blob/persistence/IBlobMetadataStore.ts | Extends metadata store interfaces with versionId support and new response types. |
| src/blob/main.ts | Creates/uses account model store and passes it into server factory for blob-only entrypoint. |
| src/blob/IBlobEnvironment.ts | Extends blob environment to include account model environment. |
| src/blob/handlers/PageBlobHandler.ts | Returns versionId on create and passes versionId through metadata store calls where applicable. |
| src/blob/handlers/ContainerHandler.ts | Adds includeVersions / includeDeletedWithVersions handling and returns version fields in listings. |
| src/blob/handlers/BlockBlobHandler.ts | Returns versionId on create/commitBlockList and threads versionId through list operations. |
| src/blob/handlers/BlobHandler.ts | Adds versionId validation, mutual-exclusion checks vs snapshot, and response versionId propagation. |
| src/blob/handlers/AppendBlobHandler.ts | Returns versionId on create and threads versionId through metadata store calls where applicable. |
| src/blob/errors/StorageErrorFactory.ts | Adds a specific error for snapshot+versionId mutual exclusivity. |
| src/blob/BlobServerFactory.ts | Threads account model store into BlobConfiguration creation. |
| src/blob/BlobServer.ts | Requires account model store on Loki path, initializes/closes it with the server. |
| src/blob/BlobEnvironment.ts | Adds CLI flags and environment integration for account model configuration. |
| src/blob/BlobConfiguration.ts | Adds accountModelStore to configuration. |
| src/blob/AccountModel.ts | Adds AccountModel interface used for account-scoped feature configuration. |
| src/azurite.ts | Wires account model store into top-level Azurite startup for blob service. |
| README.md | Documents blob versioning usage, configuration flags, and limitations. |
| package.json | Adds VS Code extension settings schema for account model configuration. |
| docs/designs/2025-12-blob-versioning.md | Adds design document describing blob versioning and account model configuration. |
Suppressed comments (2)
tests/testutils.ts:139
- The test helper builds ETags using escaped quotes ("..."), which produces backslashes in the string rather than a quoted ETag. ETags elsewhere are stored with surrounding quotes (e.g. '"test-etag"'), so this should emit actual quotes.
tests/testutils.ts:187 - The test helper builds ETags using escaped quotes ("..."), which produces backslashes in the string rather than a quoted ETag. ETags elsewhere are stored with surrounding quotes (e.g. '"test-etag"'), so this should emit actual quotes.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 53 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/common/EnvironmentFunctions.ts:51
- Single-account
--accountConfigFilePathdetection treats any entry containing ':' as multi-account, which breaks Windows paths likeC:\path\config.json(they contain a drive-letter colon). That would incorrectly parseCas an account name and fail to load the file.
if (configFilePath) {
// Check if this is single-account mode (no colon prefix) or multi-account mode
if (entries.length === 1 && !entries[0].includes(':')) {
// Single account mode: just a path without account name prefix
parseSingleAccountConfigFromPath(entries[0], accountModels);
} else {
// Multi-account mode: "accountName1:/path/to/config1.json,accountName2:/path/to/config2.json"
parseAccountConfigFromPaths(entries, accountModels);
}
src/blob/persistence/SqlBlobMetadataStore.ts:1378
- When
includeSnapshotsis true, SQL can return multiple rows with the sameblobName(differentsnapshot).PageWithDelimiternow requires a stable secondary sort key (timestamp) for identical names; returning[blobName, ""]and ordering only byblobNamewill causeadd received unsorted itemerrors at runtime.
// fill the page by possibly querying multiple times
const page = new PageWithDelimiter<BlobsModel>(maxResults, delimiter, prefix);
const nameItem = (item: BlobsModel): [string, string] => {
return [this.getModelValue<string>(item, "blobName", true), ""];
src/blob/persistence/SqlBlobMetadataStore.ts:2784
setTiernow declaresversionId: undefinedand then throws when!versionId, which makes the method always throw (breaking tier changes in SQL mode even when versioning is not involved). SQL should reject provided versionIds, but still allow non-versioned tier updates.
public setTier(
context: Context,
account: string,
container: string,
blob: string,
versionId: undefined,
tier: Models.AccessTier,
src/common/EnvironmentFunctions.ts:3
- This new file uses single-quoted imports, but the repo’s Prettier config sets singleQuote=false. Keeping quotes consistent avoids noisy diffs and formatting churn.
import { readFileSync, existsSync } from 'fs';
import { AccountModel } from '../blob/AccountModel';
import { EMULATOR_ACCOUNT_NAME } from './utils/constants';
src/blob/persistence/SqlBlobMetadataStore.ts:1352
markercan now containPageWithDelimiter.VERSIONING_MARKER+ a timestamp (used for version/snapshot-aware continuation). This SQL implementation comparesblobNamedirectly against the full marker string, which will mis-order/skip results once the marker contains the suffix.
if (markerAsTuple[0] !== undefined) {
if (whereQuery.blobName !== undefined) {
whereQuery.blobName[Op.gt] = markerAsTuple[0];
} else {
whereQuery.blobName = {
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 58 changed files in this pull request and generated no new comments.
Suppressed comments (6)
src/blob/persistence/SqlBlobMetadataStore.ts:2782
setTierin the SQL metadata store can never succeed:versionIdis typed asundefinedand the guardif (!versionId)always throws. This breaks Set Blob Tier for non-versioned blobs in SQL mode. The method should accept an optional versionId, reject non-empty version IDs (since SQL doesn’t support versioning), and proceed normally when no version is provided.
public setTier(
context: Context,
account: string,
container: string,
blob: string,
tests/common/LokiAccountModelStore.test.ts:15
substr()is deprecated; preferslice()/substring()to avoid deprecation warnings and to align with modern JS usage.
src/blob/utils/utils.ts:148- The doc comment says this parser only accepts
...fffffffZ(7 fractional digits), but the implementation intentionally accepts 3–7 fractional digits (/\.\d{3,7}Z$/). Please update the comment so callers don’t incorrectly assume 7 digits are required.
* This function will only attempt to parse strings in the specific ISO 8601 format: YYYY-MM-DDTHH:mm:ss.fffffffZ
src/common/account/LokiAccountModelStore.ts:81
- The error message in
clean()referencesLokiBlobMetadataStore, but this isLokiAccountModelStore. This makes failures harder to diagnose when the account model DB can’t be cleaned.
docs/designs/2025-12-blob-versioning.md:56 - The design doc states Azurite version IDs end in 3 fractional digits (milliseconds), but the implementation generates RFC3339 timestamps with 7 fractional digits (it pads to 7 digits and uses the last 4 digits as a per-blob counter within the same millisecond). This doc mismatch can mislead users writing versionId validation logic.
- Each version is assigned a unique version ID in ISO 8601 date-time format
- **Note:** Azurite's version IDs end in 3 digits + Z (e.g., `2024-12-06T10:30:45.123Z`) due to JavaScript's Date implementation, while Azure's version IDs end in 7 digits + Z (e.g., `2024-12-06T10:30:45.1234567Z`). If your application relies on this specific format, plan accordingly.
src/blob/handlers/BlobHandler.ts:199
- VersionId is validated for delete, but it isn’t forwarded to the metadata store. In SQL mode this means a request like
?versionid=...will delete the base blob instead of rejecting/handling a version delete (SqlBlobMetadataStore only checks itsversionIdparameter, notoptions.versionId). Passoptions.versionIdthrough when callingdeleteBlob.
this.validateVersionId(
options.snapshot,
options.versionId,
context.contextId!
);
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 58 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/common/VSCServerManagerBlob.ts:55
- VSCServerManagerBlob.createImpl no longer calls AzuriteTelemetryClient.init(), but startImpl/closeImpl still emit telemetry events. Without initialization, telemetry settings (including VSC mode/workspace config) won’t be applied and events may be dropped or mis-attributed. Also, eagerly calling accountModelStore.init() keeps the store open, which can cause BlobServer.clean() (used by the “clean” command) to fail because clean requires stores to be closed.
public async createImpl(): Promise<void> {
const env = new VSCEnvironment();
const location = await env.location();
// Create account model store
const accountModels = env.getAccountModels();
const accountModelStore = new LokiAccountModelStore(
join(location, DEFAULT_ACCOUNT_MODEL_LOKI_DB_PATH),
env.inMemoryPersistence(),
accountModels
);
await accountModelStore.init();
const blobServerFactory = new BlobServerFactory();
this.server = await blobServerFactory.createServer(env, accountModelStore);
const config = this.server.config;
src/common/IAccountModelEnvironment.ts:18
- The JSDoc
@memberoftag references IAccountDataEnvironment, but the interface name here is IAccountModelEnvironment. This is misleading in generated docs and IDE tooltips.
/**
* Gets the account models configuration from environment flags.
* Returns a map of account name to AccountModel, or undefined if no account configuration is provided.
*
* @returns {(Map<string, AccountModel> | undefined)}
* @memberof IAccountDataEnvironment
*/
getAccountModels(): Map<string, AccountModel> | undefined;
src/blob/BlobServer.ts:196
- BlobServer.clean() now unconditionally calls accountModelStore.clean(), but LokiAccountModelStore.clean() throws unless the store is closed. This makes
server.clean()fail in valid scenarios where the server is in Closed status but the account model store was initialized (e.g., VS Code manager calls createImpl then clean without starting/closing). Close the account model store (if open) before cleaning it, consistent with other stores’ expectations.
if (this.metadataStore !== undefined) {
await this.metadataStore.clean();
}
await this.accountModelStore.clean();
Title: Add Blob Versioning Support to Azurite
Summary
Adds Azure Blob Storage versioning support to Azurite, enabling applications to create, list, retrieve, and delete previous blob versions during local development.
Closes #665.
Account model
This work introduces the account model abstraction designed in collaboration with the Azurite team. The account model provides a central place for account-scoped feature configuration so additional Azure Storage capabilities can be added without coupling them directly to server startup or individual blob handlers.
Blob versioning is currently the first and only feature backed by this abstraction. The design supports Azurite's multi-account mode, allowing each configured account to enable or disable versioning independently. Accounts without an explicit versioning setting retain the existing non-versioned behavior for backward compatibility.
Motivation
Blob versioning automatically preserves previous blob states after modification or deletion. Without emulator support, applications that depend on version-aware workflows cannot be tested locally before deployment to Azure.
Implementation
AccountModelsetting for enabling blob versioning.versionId.includeVersionswith version-aware continuation markers.Configuration
Blob versioning can be configured using:
--accountConfigFilePathfor JSON configuration files.--accountConfigAsJsonfor inline JSON configuration.Configured accounts must also be present in
AZURITE_ACCOUNTSfor authentication.Behavior
Limitations
This change does not currently support:
Validation