Skip to content

Add Blob Versioning Support to Azurite - #2735

Open
Rodolfo Orozco Vasquez (rorozcov) wants to merge 70 commits into
Azure:mainfrom
rorozcov:users/rorozcov/blobversioning
Open

Add Blob Versioning Support to Azurite#2735
Rodolfo Orozco Vasquez (rorozcov) wants to merge 70 commits into
Azure:mainfrom
rorozcov:users/rorozcov/blobversioning

Conversation

@rorozcov

Copy link
Copy Markdown

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

  • Adds an account-level AccountModel setting for enabling blob versioning.
  • Supports file-based and inline JSON account configuration, including multiple accounts.
  • Persists account configuration in Loki metadata storage.
  • Creates version IDs as ISO 8601 timestamps when version-producing blob operations occur.
  • Supports retrieving and deleting a specific version through versionId.
  • Supports listing versions through includeVersions with version-aware continuation markers.
  • Preserves previous versions as immutable records while tracking the current version.
  • Adds versioning behavior across block, append, and page blob operations.
  • Adds VS Code extension settings for account configuration.
  • Preserves backward compatibility by defaulting accounts without versioning configuration to versioning disabled.

Configuration

Blob versioning can be configured using:

  • --accountConfigFilePath for JSON configuration files.
  • --accountConfigAsJson for inline JSON configuration.

Configured accounts must also be present in AZURITE_ACCOUNTS for authentication.

Behavior

  • Block blob writes create versions except for Put Block.
  • Page and append blobs create versions for Put Blob, Put Block List, Set Blob Metadata, and Copy Blob.
  • Put Page and Append Block do not create versions.
  • Previous versions can be read or deleted using their version ID.
  • Version IDs use JavaScript millisecond precision rather than Azure's seven fractional digits.

Limitations

This change does not currently support:

  • Soft-delete integration
  • Blob expiration with versioning
  • SAS URIs targeting specific versions
  • Version-level immutability policies

Validation

  • TypeScript build and lint pass.
  • Blob version pagination tests pass.
  • Startup and persisted-data upgrade regression tests pass with versioning disabled.

…tore. Now local testing needed. Saving progress
…tests with versioning enabled. Must add versioning related checks
Copilot AI lite review requested due to automatic review settings August 13, 2026 16:11
@rorozcov

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + LokiAccountModelStore and 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, and versionId query 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.

Comment thread src/common/EnvironmentFunctions.ts
Comment thread src/blob/persistence/SqlBlobMetadataStore.ts Outdated
Comment thread src/blob/persistence/SqlBlobMetadataStore.ts
Comment thread src/common/account/LokiAccountModelStore.ts Outdated
Comment thread docs/designs/2025-12-blob-versioning.md
Comment thread tests/testutils.ts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 13, 2026 16:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --accountConfigFilePath detection treats any entry containing ':' as multi-account, which breaks Windows paths like C:\path\config.json (they contain a drive-letter colon). That would incorrectly parse C as 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 includeSnapshots is true, SQL can return multiple rows with the same blobName (different snapshot). PageWithDelimiter now requires a stable secondary sort key (timestamp) for identical names; returning [blobName, ""] and ordering only by blobName will cause add received unsorted item errors 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

  • setTier now declares versionId: undefined and 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

  • marker can now contain PageWithDelimiter.VERSIONING_MARKER + a timestamp (used for version/snapshot-aware continuation). This SQL implementation compares blobName directly 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>
Copilot AI review requested due to automatic review settings August 13, 2026 23:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • setTier in the SQL metadata store can never succeed: versionId is typed as undefined and the guard if (!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; prefer slice()/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() references LokiBlobMetadataStore, but this is LokiAccountModelStore. 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 its versionId parameter, not options.versionId). Pass options.versionId through when calling deleteBlob.
    this.validateVersionId(
      options.snapshot,
      options.versionId,
      context.contextId!
    );

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 13, 2026 23:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @memberof tag 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();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable Blob versioning

2 participants