diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 000000000..fca24df4c
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "trailingComma": "none",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": false
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e6cb6bba5..0143bafd2 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,5 @@
{
"editor.tabSize": 2,
- "editor.formatOnSave": true
+ "editor.formatOnSave": true,
+ "typescript.tsdk": "node_modules/typescript/lib"
}
diff --git a/ChangeLog.md b/ChangeLog.md
index 62a983cc9..69573d74b 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -2,6 +2,15 @@
> Note. This file includes changes after 3.0.0-preview. For legacy Azurite changes, please goto GitHub [releases](https://github.com/Azure/Azurite/releases).
+## 2020.03 Version 3.6.0
+
+- Supported conditional headers.
+- Compatible with upper case or lower case of x-ms-sequence-number-action values.
+- Fixed issue that x-ms-blob-sequence-number of 0 should be returned for HEAD requests on Page blob.
+- Uploading blocks with different lengths of IDs to the same blob will fail.
+- Check if block blob exists should fail if blocks are all uncommitted.
+- Case sensitive with metadata keys.
+
## 2020.02 Version 3.5.0
- Bump up Azure Storage service API version to 2019-07-07.
diff --git a/README.md b/README.md
index 740751820..aabee7c77 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
| Version | Azure Storage API Version | Service Support | Description | Reference Links |
| ------------------------------------------------------------------ | ------------------------- | --------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 3.5.0 | 2019-07-07 | Blob
Queue | Azurite V3 based on TypeScript & New Architecture | [NPM](https://www.npmjs.com/package/azurite) - [Docker](https://hub.docker.com/_/microsoft-azure-storage-azurite) - [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite) |
+| 3.6.0 | 2019-07-07 | Blob
Queue | Azurite V3 based on TypeScript & New Architecture | [NPM](https://www.npmjs.com/package/azurite) - [Docker](https://hub.docker.com/_/microsoft-azure-storage-azurite) - [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite) |
| [Legacy (v2)](https://github.com/Azure/Azurite/tree/legacy-master) | 2016-05-31 | Blob, Queue and Table | Legacy Azurite V2 | [NPM](https://www.npmjs.com/package/azurite) |
## Introduction
@@ -569,7 +569,7 @@ All the generated code is kept in `generated` folder, including the generated mi
## Support Matrix
-3.5.0 release targets **2019-07-07** API version **blob** service.
+Latest release targets **2019-07-07** API version **blob** service.
Detailed support matrix:
- Supported Vertical Features
@@ -603,19 +603,19 @@ Detailed support matrix:
- Snapshot Blob
- Copy Blob (Only supports copy within same account in Azurite)
- Abort Copy Blob (Only supports copy within same account in Azurite)
+ - Access control based on conditional headers
- Following features or REST APIs are NOT supported or limited supported in this release (will support more features per customers feedback in future releases)
- SharedKey Lite
- OAuth authentication
- - Access control based on conditional headers (Requests will be blocked in strict mode)
- Static Website
- Soft delete & Undelete Blob
- Put Block from URL
- Incremental Copy Blob
- Create Append Blob, Append Block
- 3.5.0 release added support for **2019-07-07** API version **queue** service.
- Detailed support matrix:
+Latest version supports for **2019-07-07** API version **queue** service.
+Detailed support matrix:
- Supported Vertical Features
- SharedKey Authentication
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 3111963e7..6491627fd 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -70,7 +70,7 @@ jobs:
- job: blobtestmac
displayName: Blob Test Mac
pool:
- vmImage: "macOS-10.13"
+ vmImage: "macOS-10.14"
strategy:
matrix:
node_8_x:
@@ -194,7 +194,7 @@ jobs:
- job: queuetestmac
displayName: Queue Test Mac
pool:
- vmImage: "macOS-10.13"
+ vmImage: "macOS-10.14"
strategy:
matrix:
node_8_x:
@@ -319,7 +319,7 @@ jobs:
- job: azuritenodejsmac
displayName: Azurite Mac
pool:
- vmImage: "macOS-10.13"
+ vmImage: "macOS-10.14"
strategy:
matrix:
node_8_x:
diff --git a/package-lock.json b/package-lock.json
index 9df261c5b..f3fefdf34 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "azurite",
- "version": "3.5.0",
+ "version": "3.6.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -180,6 +180,22 @@
}
}
},
+ "@mrmlnc/readdir-enhanced": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
+ "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==",
+ "dev": true,
+ "requires": {
+ "call-me-maybe": "^1.0.1",
+ "glob-to-regexp": "^0.3.0"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
+ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
+ "dev": true
+ },
"@samverschueren/stream-to-observable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
@@ -1754,6 +1770,12 @@
"unset-value": "^1.0.0"
}
},
+ "call-me-maybe": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+ "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
+ "dev": true
+ },
"caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
@@ -1869,6 +1891,97 @@
"string-width": "^1.0.1"
}
},
+ "cliui": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+ "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^2.1.1",
+ "strip-ansi": "^4.0.0",
+ "wrap-ansi": "^2.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ }
+ }
+ }
+ }
+ },
"cls-bluebird": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz",
@@ -2151,6 +2264,12 @@
"ms": "2.0.0"
}
},
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -2283,6 +2402,16 @@
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
+ "dir-glob": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz",
+ "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==",
+ "dev": true,
+ "requires": {
+ "arrify": "^1.0.1",
+ "path-type": "^3.0.0"
+ }
+ },
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
@@ -2700,6 +2829,20 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
},
+ "fast-glob": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
+ "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==",
+ "dev": true,
+ "requires": {
+ "@mrmlnc/readdir-enhanced": "^2.2.1",
+ "@nodelib/fs.stat": "^1.1.2",
+ "glob-parent": "^3.1.0",
+ "is-glob": "^4.0.0",
+ "merge2": "^1.2.3",
+ "micromatch": "^3.1.10"
+ }
+ },
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2873,6 +3016,12 @@
"is-property": "^1.0.2"
}
},
+ "get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "dev": true
+ },
"get-own-enumerable-property-symbols": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz",
@@ -2921,6 +3070,33 @@
"path-is-absolute": "^1.0.0"
}
},
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ }
+ }
+ },
+ "glob-to-regexp": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz",
+ "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
+ "dev": true
+ },
"globals": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
@@ -3138,6 +3314,12 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
+ "ignore": {
+ "version": "3.3.10",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+ "dev": true
+ },
"import-fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@@ -3182,6 +3364,12 @@
"loose-envify": "^1.0.0"
}
},
+ "invert-kv": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+ "dev": true
+ },
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
@@ -3578,9 +3766,9 @@
}
},
"kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true
},
"kuler": {
@@ -3591,6 +3779,15 @@
"colornames": "^1.1.1"
}
},
+ "lcid": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+ "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+ "dev": true,
+ "requires": {
+ "invert-kv": "^2.0.0"
+ }
+ },
"leven": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
@@ -3831,6 +4028,15 @@
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
"dev": true
},
+ "map-age-cleaner": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+ "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+ "dev": true,
+ "requires": {
+ "p-defer": "^1.0.0"
+ }
+ },
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -3879,11 +4085,36 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
+ "mem": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
+ "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
+ "dev": true,
+ "requires": {
+ "map-age-cleaner": "^0.1.1",
+ "mimic-fn": "^2.0.0",
+ "p-is-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ }
+ }
+ },
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
+ "merge2": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
+ "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==",
+ "dev": true
+ },
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -4372,6 +4603,17 @@
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true
},
+ "os-locale": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+ "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+ "dev": true,
+ "requires": {
+ "execa": "^1.0.0",
+ "lcid": "^2.0.0",
+ "mem": "^4.0.0"
+ }
+ },
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@@ -4388,12 +4630,24 @@
"os-tmpdir": "^1.0.0"
}
},
+ "p-defer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+ "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
+ "dev": true
+ },
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true
},
+ "p-is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
+ "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
+ "dev": true
+ },
"p-limit": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
@@ -4471,6 +4725,12 @@
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
"dev": true
},
+ "path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+ "dev": true
+ },
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@@ -4505,6 +4765,15 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
+ "path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "requires": {
+ "pify": "^3.0.0"
+ }
+ },
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -4567,6 +4836,37 @@
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
"dev": true
},
+ "prettier-tslint": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/prettier-tslint/-/prettier-tslint-0.4.2.tgz",
+ "integrity": "sha512-urhX7U/F+fu8sztEs/Z7CxNS8PdEytEwGKhQaH5fxxCdRmHGT45FoClyDlcZrMk9cK/8JpX/asFmTOHtSGJfLg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.0",
+ "globby": "^8.0.1",
+ "ignore": "^3.3.7",
+ "require-relative": "^0.8.7",
+ "tslint": "^5.9.1",
+ "yargs": "^11.0.0"
+ },
+ "dependencies": {
+ "globby": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz",
+ "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==",
+ "dev": true,
+ "requires": {
+ "array-union": "^1.0.1",
+ "dir-glob": "2.0.0",
+ "fast-glob": "^2.0.2",
+ "glob": "^7.1.2",
+ "ignore": "^3.3.5",
+ "pify": "^3.0.0",
+ "slash": "^1.0.0"
+ }
+ }
+ }
+ },
"private": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@@ -4819,6 +5119,24 @@
}
}
},
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+ "dev": true
+ },
+ "require-relative": {
+ "version": "0.8.7",
+ "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
+ "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
+ "dev": true
+ },
"resolve": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
@@ -5022,6 +5340,12 @@
"send": "0.17.1"
}
},
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
"set-value": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@@ -5967,6 +6291,12 @@
"isexe": "^2.0.0"
}
},
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
"winston": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz",
@@ -6092,11 +6422,130 @@
"resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz",
"integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ=="
},
+ "y18n": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+ "dev": true
+ },
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
+ "yargs": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz",
+ "integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==",
+ "dev": true,
+ "requires": {
+ "cliui": "^4.0.0",
+ "decamelize": "^1.1.1",
+ "find-up": "^2.1.0",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^3.1.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^9.0.2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "dev": true,
+ "requires": {
+ "locate-path": "^2.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "dev": true,
+ "requires": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "dev": true,
+ "requires": {
+ "p-try": "^1.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "dev": true,
+ "requires": {
+ "p-limit": "^1.1.0"
+ }
+ },
+ "p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz",
+ "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^4.1.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+ "dev": true
+ }
+ }
+ },
"yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
diff --git a/package.json b/package.json
index be1fcf689..02a256e89 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Azurite",
"description": "An open source Azure Storage API compatible server",
"icon": "icon.png",
- "version": "3.5.0",
+ "version": "3.6.0",
"publisher": "Azurite",
"categories": [
"Other"
@@ -37,7 +37,6 @@
"xml2js": "^0.4.19"
},
"devDependencies": {
- "@types/validator": "^10.11.3",
"@azure/storage-blob": "^10.4.1",
"@azure/storage-queue": "^10.2.0",
"@types/args": "^3.0.0",
@@ -53,6 +52,7 @@
"@types/rimraf": "^2.0.2",
"@types/uri-templates": "^0.1.29",
"@types/uuid": "^3.4.4",
+ "@types/validator": "^10.11.3",
"@types/vscode": "^1.39.0",
"@types/xml2js": "^0.4.3",
"autorest": "^2.0.4283",
@@ -62,6 +62,7 @@
"lint-staged": "^8.1.5",
"mocha": "^5.2.0",
"prettier": "^1.16.4",
+ "prettier-tslint": "^0.4.2",
"ts-node": "^7.0.1",
"tslint": "^5.16.0",
"typescript": "^3.1.4",
diff --git a/src/blob/authentication/BlobTokenAuthenticator.ts b/src/blob/authentication/BlobTokenAuthenticator.ts
new file mode 100644
index 000000000..90d256691
--- /dev/null
+++ b/src/blob/authentication/BlobTokenAuthenticator.ts
@@ -0,0 +1,68 @@
+import IAccountDataStore from "../../common/IAccountDataStore";
+import ILogger from "../../common/ILogger";
+import BlobStorageContext from "../context/BlobStorageContext";
+import StorageErrorFactory from "../errors/StorageErrorFactory";
+import Context from "../generated/Context";
+import IRequest from "../generated/IRequest";
+import { HeaderConstants } from "../utils/constants";
+import IAuthenticator from "./IAuthenticator";
+
+export default class BlobTokenAuthenticator implements IAuthenticator {
+ public constructor(
+ private readonly dataStore: IAccountDataStore,
+ private readonly logger: ILogger
+ ) {}
+
+ public async validate(
+ req: IRequest,
+ context: Context
+ ): Promise {
+ const blobContext = new BlobStorageContext(context);
+ const account = blobContext.account!;
+
+ this.logger.info(
+ `BlobTokenAuthenticator:validate() Start validation against token authentication.`,
+ blobContext.contextId
+ );
+
+ // TODO: Make following async
+ const accountProperties = this.dataStore.getAccount(account);
+ if (accountProperties === undefined) {
+ this.logger.error(
+ `BlobTokenAuthenticator:validate() Invalid storage account ${account}.`,
+ blobContext.contextId
+ );
+ throw StorageErrorFactory.getInvalidOperation(
+ blobContext.contextId!,
+ "Invalid storage account."
+ );
+ }
+
+ const authHeaderValue = req.getHeader(HeaderConstants.AUTHORIZATION);
+ if (authHeaderValue === undefined) {
+ this.logger.info(
+ // tslint:disable-next-line:max-line-length
+ `BlobTokenAuthenticator:validate() Request doesn't include valid authentication header. Skip token authentication.`,
+ blobContext.contextId
+ );
+ return undefined;
+ } else {
+ const hasBearerToken = authHeaderValue.startsWith("Bearer");
+
+ if (hasBearerToken) {
+ this.logger.info(
+ // tslint:disable-next-line:max-line-length
+ `BlobTokenAuthenticator:validate() Request includes Bearer token.`,
+ blobContext.contextId
+ );
+ } else {
+ this.logger.info(
+ // tslint:disable-next-line:max-line-length
+ `BlobTokenAuthenticator:validate() Request does not include Bearer token. Skip token authentication.`,
+ blobContext.contextId
+ );
+ }
+ return hasBearerToken;
+ }
+ }
+}
diff --git a/src/blob/conditions/ConditionResourceAdapter.ts b/src/blob/conditions/ConditionResourceAdapter.ts
new file mode 100644
index 000000000..2cc0144db
--- /dev/null
+++ b/src/blob/conditions/ConditionResourceAdapter.ts
@@ -0,0 +1,37 @@
+import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore";
+import IConditionResource from "./IConditionResource";
+
+export default class ConditionResourceAdapter implements IConditionResource {
+ public exist: boolean;
+ public etag: string;
+ public lastModified: Date;
+
+ public constructor(resource: BlobModel | ContainerModel | undefined | null) {
+ if (
+ resource === undefined ||
+ resource === null ||
+ (resource as BlobModel).isCommitted === false // Treat uncommitted blob as unexist resource
+ ) {
+ this.exist = false;
+ this.etag = "UNEXIST_RESOURCE_ETAG";
+ this.lastModified = undefined as any;
+ return;
+ }
+
+ this.exist = true;
+ this.etag = resource.properties.etag;
+
+ if (this.etag.length < 3) {
+ throw new Error(
+ `ConditionResourceAdapter::constructor() Invalid etag ${this.etag}.`
+ );
+ }
+
+ if (this.etag.startsWith('"') && this.etag.endsWith('"')) {
+ this.etag = this.etag.substring(1, this.etag.length - 1);
+ }
+
+ this.lastModified = new Date(resource.properties.lastModified);
+ this.lastModified.setMilliseconds(0); // Precision to seconds
+ }
+}
diff --git a/src/blob/conditions/ConditionalHeadersAdapter.ts b/src/blob/conditions/ConditionalHeadersAdapter.ts
new file mode 100644
index 000000000..40ef469ec
--- /dev/null
+++ b/src/blob/conditions/ConditionalHeadersAdapter.ts
@@ -0,0 +1,47 @@
+import { ModifiedAccessConditions } from "../generated/artifacts/models";
+import Context from "../generated/Context";
+import { IConditionalHeaders } from "./IConditionalHeaders";
+
+export default class ConditionalHeadersAdapter implements IConditionalHeaders {
+ public ifModifiedSince?: Date;
+ public ifUnmodifiedSince?: Date;
+ public ifMatch?: string[];
+ public ifNoneMatch?: string[];
+
+ public constructor(
+ context: Context,
+ modifiedAccessConditions: ModifiedAccessConditions = {}
+ ) {
+ // If-Match & If-None-Match allow multi values separated by comma
+ if (modifiedAccessConditions.ifMatch) {
+ this.ifMatch = modifiedAccessConditions.ifMatch.split(",").map(etag => {
+ if (etag.startsWith('"') && etag.endsWith('"')) {
+ return etag.substring(1, etag.length - 1);
+ }
+ return etag;
+ });
+ }
+
+ if (modifiedAccessConditions.ifNoneMatch) {
+ this.ifNoneMatch = modifiedAccessConditions.ifNoneMatch
+ .split(",")
+ .map(etag => {
+ if (etag.startsWith('"') && etag.endsWith('"')) {
+ return etag.substring(1, etag.length - 1);
+ }
+ return etag;
+ });
+ }
+
+ // If-Modified-Since & If-Unmodified-Since don't support multi values
+ this.ifModifiedSince = modifiedAccessConditions.ifModifiedSince;
+ if (this.ifModifiedSince) {
+ this.ifModifiedSince.setMilliseconds(0); // Precision to seconds
+ }
+
+ this.ifUnmodifiedSince = modifiedAccessConditions.ifUnmodifiedSince;
+ if (this.ifUnmodifiedSince) {
+ this.ifUnmodifiedSince.setMilliseconds(0); // Precision to seconds
+ }
+ }
+}
diff --git a/src/blob/conditions/IConditionResource.ts b/src/blob/conditions/IConditionResource.ts
new file mode 100644
index 000000000..dda91509c
--- /dev/null
+++ b/src/blob/conditions/IConditionResource.ts
@@ -0,0 +1,16 @@
+export default interface IConditionResource {
+ /**
+ * Whether resource exists or not.
+ */
+ exist: boolean;
+
+ /**
+ * etag string without quotes.
+ */
+ etag: string;
+
+ /**
+ * last modified time for container or blob.
+ */
+ lastModified: Date;
+}
diff --git a/src/blob/conditions/IConditionalHeaders.ts b/src/blob/conditions/IConditionalHeaders.ts
new file mode 100644
index 000000000..1749fca54
--- /dev/null
+++ b/src/blob/conditions/IConditionalHeaders.ts
@@ -0,0 +1,14 @@
+export interface IConditionalHeaders {
+ ifModifiedSince?: Date;
+ ifUnmodifiedSince?: Date;
+
+ /**
+ * If-Match etag list without quotes.
+ */
+ ifMatch?: string[];
+
+ /**
+ * If-None-Match etag list without quotes.
+ */
+ ifNoneMatch?: string[];
+}
diff --git a/src/blob/conditions/IConditionalHeadersValidator.ts b/src/blob/conditions/IConditionalHeadersValidator.ts
new file mode 100644
index 000000000..7a8c04431
--- /dev/null
+++ b/src/blob/conditions/IConditionalHeadersValidator.ts
@@ -0,0 +1,11 @@
+import Context from "../generated/Context";
+import { IConditionalHeaders } from "./IConditionalHeaders";
+import IConditionResource from "./IConditionResource";
+
+export interface IConditionalHeadersValidator {
+ validate(
+ context: Context,
+ conditionalHeaders: IConditionalHeaders,
+ resource: IConditionResource
+ ): void;
+}
diff --git a/src/blob/conditions/ReadConditionalHeadersValidator.ts b/src/blob/conditions/ReadConditionalHeadersValidator.ts
new file mode 100644
index 000000000..6943d5f5e
--- /dev/null
+++ b/src/blob/conditions/ReadConditionalHeadersValidator.ts
@@ -0,0 +1,112 @@
+import StorageErrorFactory from "../errors/StorageErrorFactory";
+import { ModifiedAccessConditions } from "../generated/artifacts/models";
+import Context from "../generated/Context";
+import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore";
+import ConditionalHeadersAdapter from "./ConditionalHeadersAdapter";
+import ConditionResourceAdapter from "./ConditionResourceAdapter";
+import { IConditionalHeaders } from "./IConditionalHeaders";
+import { IConditionalHeadersValidator } from "./IConditionalHeadersValidator";
+import IConditionResource from "./IConditionResource";
+
+export function validateReadConditions(
+ context: Context,
+ conditionalHeaders?: ModifiedAccessConditions,
+ model?: BlobModel | ContainerModel | null
+) {
+ new ReadConditionalHeadersValidator().validate(
+ context,
+ new ConditionalHeadersAdapter(context, conditionalHeaders),
+ new ConditionResourceAdapter(model)
+ );
+}
+
+// tslint:disable: max-line-length
+export default class ReadConditionalHeadersValidator
+ implements IConditionalHeadersValidator {
+ /**
+ * Validate Conditional Headers for Blob Service Read Operations in Version 2013-08-15 or Later.
+ * @link https://docs.microsoft.com/en-us/rest/api/storageservices/specifying-conditional-headers-for-blob-service-operations#specifying-conditional-headers-for-blob-service-read-operations-in-version-2013-08-15-or-later
+ *
+ * @param context
+ * @param conditionalHeaders
+ * @param resource
+ */
+ public validate(
+ context: Context,
+ conditionalHeaders: IConditionalHeaders,
+ resource: IConditionResource
+ ): void {
+ // If-Match && If-Unmodified-Since && (If-None-Match || If-Modified-Since)
+
+ // Read against a non exist resource
+ if (!resource.exist) {
+ // If-Match
+ if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+
+ // If If-Unmodified-Since
+ // Skip for unexist resource
+
+ // If-None-Match
+ if (
+ conditionalHeaders.ifNoneMatch &&
+ conditionalHeaders.ifNoneMatch.length > 0 &&
+ conditionalHeaders.ifNoneMatch[0] === "*"
+ ) {
+ throw StorageErrorFactory.getUnsatisfiableCondition(context.contextId!);
+ }
+
+ // If-Modified-Since
+ // Skip for unexist resource
+ } else {
+ // Read against an existing resource
+ // If-Match && If-Unmodified-Since && (If-None-Match || If-Modified-Since)
+
+ // If-Match
+ const ifMatchPass = conditionalHeaders.ifMatch
+ ? conditionalHeaders.ifMatch.includes(resource.etag) ||
+ conditionalHeaders.ifMatch[0] === "*"
+ : undefined;
+
+ // If-Unmodified-Since
+ const ifUnModifiedSincePass = conditionalHeaders.ifUnmodifiedSince
+ ? resource.lastModified <= conditionalHeaders.ifUnmodifiedSince
+ : undefined;
+
+ // If-None-Match
+ if (
+ conditionalHeaders.ifNoneMatch &&
+ conditionalHeaders.ifNoneMatch.length > 0 &&
+ conditionalHeaders.ifNoneMatch[0] === "*"
+ ) {
+ throw StorageErrorFactory.getUnsatisfiableCondition(context.contextId!);
+ }
+
+ const ifNoneMatchPass = conditionalHeaders.ifNoneMatch
+ ? !conditionalHeaders.ifNoneMatch.includes(resource.etag)
+ : undefined;
+
+ // If-Modified-Since
+ const isModifiedSincePass = conditionalHeaders.ifModifiedSince
+ ? conditionalHeaders.ifModifiedSince < resource.lastModified
+ : undefined;
+
+ if (ifMatchPass === false) {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+
+ if (ifUnModifiedSincePass === false) {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+
+ if (ifNoneMatchPass === false && isModifiedSincePass !== true) {
+ throw StorageErrorFactory.getNotModified(context.contextId!);
+ }
+
+ if (isModifiedSincePass === false && ifNoneMatchPass !== true) {
+ throw StorageErrorFactory.getNotModified(context.contextId!);
+ }
+ }
+ }
+}
diff --git a/src/blob/conditions/WriteConditionalHeadersValidator.ts b/src/blob/conditions/WriteConditionalHeadersValidator.ts
new file mode 100644
index 000000000..15cf67728
--- /dev/null
+++ b/src/blob/conditions/WriteConditionalHeadersValidator.ts
@@ -0,0 +1,235 @@
+import StorageErrorFactory from "../errors/StorageErrorFactory";
+import {
+ ModifiedAccessConditions,
+ SequenceNumberAccessConditions
+} from "../generated/artifacts/models";
+import Context from "../generated/Context";
+import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore";
+import ConditionalHeadersAdapter from "./ConditionalHeadersAdapter";
+import ConditionResourceAdapter from "./ConditionResourceAdapter";
+import { IConditionalHeaders } from "./IConditionalHeaders";
+import { IConditionalHeadersValidator } from "./IConditionalHeadersValidator";
+import IConditionResource from "./IConditionResource";
+
+export function validateSequenceNumberWriteConditions(
+ context: Context,
+ conditionalHeaders?: SequenceNumberAccessConditions,
+ model?: BlobModel
+) {
+ if (!conditionalHeaders || !model) {
+ return;
+ }
+
+ if (!model.properties || model.properties.blobSequenceNumber === undefined) {
+ throw Error(
+ `validateSequenceNumberWriteConditions() Invalid blob model, blobSequenceNumber is not specified.`
+ );
+ }
+
+ if (
+ conditionalHeaders.ifSequenceNumberLessThanOrEqualTo !== undefined &&
+ conditionalHeaders.ifSequenceNumberLessThanOrEqualTo <
+ model.properties.blobSequenceNumber
+ ) {
+ throw StorageErrorFactory.getSequenceNumberConditionNotMet(
+ context.contextId!
+ );
+ }
+
+ if (
+ conditionalHeaders.ifSequenceNumberLessThan !== undefined &&
+ conditionalHeaders.ifSequenceNumberLessThan <=
+ model.properties.blobSequenceNumber
+ ) {
+ throw StorageErrorFactory.getSequenceNumberConditionNotMet(
+ context.contextId!
+ );
+ }
+
+ if (
+ conditionalHeaders.ifSequenceNumberEqualTo !== undefined &&
+ conditionalHeaders.ifSequenceNumberEqualTo !==
+ model.properties.blobSequenceNumber
+ ) {
+ throw StorageErrorFactory.getSequenceNumberConditionNotMet(
+ context.contextId!
+ );
+ }
+}
+
+export function validateWriteConditions(
+ context: Context,
+ conditionalHeaders?: ModifiedAccessConditions,
+ model?: BlobModel | ContainerModel | null
+) {
+ new WriteConditionalHeadersValidator().validate(
+ context,
+ new ConditionalHeadersAdapter(context, conditionalHeaders),
+ new ConditionResourceAdapter(model)
+ );
+}
+
+// tslint:disable: max-line-length
+export default class WriteConditionalHeadersValidator
+ implements IConditionalHeadersValidator {
+ /**
+ * Validate conditional Headers for Read Operations in Versions Prior to 2013-08-15,
+ * and for Write Operations (All Versions).
+ * @link https://docs.microsoft.com/en-us/rest/api/storageservices/specifying-conditional-headers-for-blob-service-operations#specifying-conditional-headers-for-read-operations-in-versions-prior-to-2013-08-15-and-for-write-operations-all-versions
+ *
+ * @param context
+ * @param conditionalHeaders
+ * @param resource
+ */
+ public validate(
+ context: Context,
+ conditionalHeaders: IConditionalHeaders,
+ resource: IConditionResource
+ ): void {
+ this.validateCombinations(context, conditionalHeaders);
+ if (!resource.exist) {
+ if (
+ conditionalHeaders.ifNoneMatch &&
+ conditionalHeaders.ifNoneMatch.length > 0
+ ) {
+ // If a request specifies both the If-None-Match and If-Modified-Since headers,
+ // the request is evaluated based on the criteria specified in If-None-Match.
+ // Skip for non exist blob
+ return;
+ }
+
+ if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) {
+ // If a request specifies both the If-Match and If-Unmodified-Since headers,
+ // the request is evaluated based on the criteria specified in If-Match.
+ if (conditionalHeaders.ifMatch[0] !== "*") {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+ return;
+ }
+
+ if (conditionalHeaders.ifModifiedSince) {
+ // Skip for non exist blob
+ return;
+ }
+
+ if (conditionalHeaders.ifUnmodifiedSince) {
+ // Skip for non exist blob
+ return;
+ }
+ } else {
+ if (
+ conditionalHeaders.ifNoneMatch &&
+ conditionalHeaders.ifNoneMatch.length > 0
+ ) {
+ if (conditionalHeaders.ifNoneMatch[0] === "*") {
+ // According to restful doc, specify the wildcard character (*) to perform the operation
+ // only if the resource does not exist, and fail the operation if it does exist.
+ // However, Azure Storage Set Blob Properties Operation for an existing blob doesn't reuturn 412 with *
+ // TODO: Check accurate behavior for different write operations
+ // Put Blob, Commit Block List has special logic for ifNoneMatch equals *, will return 409 conflict for existing blob, will handled in createBlob metatdata store.
+ // throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ return;
+ }
+ if (conditionalHeaders.ifNoneMatch[0] === resource.etag) {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+
+ // Stop processing
+ // If a request specifies both the If-None-Match and If-Modified-Since headers,
+ // the request is evaluated based on the criteria specified in If-None-Match.
+ return;
+ }
+
+ if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) {
+ if (
+ conditionalHeaders.ifMatch[0] !== "*" &&
+ conditionalHeaders.ifMatch[0] !== resource.etag
+ ) {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+
+ // Stop processing
+ // If a request specifies both the If-Match and If-Unmodified-Since headers,
+ // the request is evaluated based on the criteria specified in If-Match.
+ return;
+ }
+
+ if (conditionalHeaders.ifModifiedSince) {
+ if (resource.lastModified <= conditionalHeaders.ifModifiedSince) {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+ return;
+ }
+
+ if (conditionalHeaders.ifUnmodifiedSince) {
+ if (conditionalHeaders.ifUnmodifiedSince < resource.lastModified) {
+ throw StorageErrorFactory.getConditionNotMet(context.contextId!);
+ }
+ return;
+ }
+ }
+ }
+
+ private validateCombinations(
+ context: Context,
+ conditionalHeaders: IConditionalHeaders
+ ) {
+ let ifMatch = 0;
+ if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) {
+ // RFC 2616 allows multiple ETag values in a single header,
+ // but requests to the Blob service can only include one ETag value.
+ // Specifying more than one ETag value results in status code 400 (Bad Request).
+ if (conditionalHeaders.ifMatch.length > 1) {
+ // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported.
+ throw StorageErrorFactory.getMultipleConditionHeadersNotSupported(
+ context.contextId!
+ );
+ }
+
+ ifMatch = 1;
+ }
+
+ let ifModifiedSince = 0;
+ if (conditionalHeaders.ifModifiedSince) {
+ ifModifiedSince = 1;
+ }
+
+ let ifNoneMatch = 0;
+ if (
+ conditionalHeaders.ifNoneMatch &&
+ conditionalHeaders.ifNoneMatch.length > 0
+ ) {
+ // RFC 2616 allows multiple ETag values in a single header,
+ // but requests to the Blob service can only include one ETag value.
+ // Specifying more than one ETag value results in status code 400 (Bad Request).
+ if (conditionalHeaders.ifNoneMatch.length > 1) {
+ // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported.
+ throw StorageErrorFactory.getMultipleConditionHeadersNotSupported(
+ context.contextId!
+ );
+ }
+ ifNoneMatch = 1;
+ }
+
+ let ifUnmodifiedSince = 0;
+ if (conditionalHeaders.ifUnmodifiedSince) {
+ ifUnmodifiedSince = 1;
+ }
+
+ if (ifMatch + ifModifiedSince + ifNoneMatch + ifUnmodifiedSince > 2) {
+ // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported.
+ throw StorageErrorFactory.getMultipleConditionHeadersNotSupported(
+ context.contextId!
+ );
+ }
+
+ if (ifMatch + ifModifiedSince + ifNoneMatch + ifUnmodifiedSince === 2) {
+ if (ifNoneMatch + ifModifiedSince === 1) {
+ // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported.
+ throw StorageErrorFactory.getMultipleConditionHeadersNotSupported(
+ context.contextId!
+ );
+ }
+ }
+ }
+}
diff --git a/src/blob/errors/StorageError.ts b/src/blob/errors/StorageError.ts
index 00ef46fac..a1a24d899 100644
--- a/src/blob/errors/StorageError.ts
+++ b/src/blob/errors/StorageError.ts
@@ -9,6 +9,10 @@ import { jsonToXML } from "../generated/utils/xml";
* @extends {MiddlewareError}
*/
export default class StorageError extends MiddlewareError {
+ public readonly storageErrorCode: string;
+ public readonly storageErrorMessage: string;
+ public readonly storageRequestID: string;
+
/**
* Creates an instance of StorageError.
*
@@ -54,5 +58,8 @@ export default class StorageError extends MiddlewareError {
);
this.name = "StorageError";
+ this.storageErrorCode = storageErrorCode;
+ this.storageErrorMessage = storageErrorMessage;
+ this.storageRequestID = storageRequestID;
}
}
diff --git a/src/blob/errors/StorageErrorFactory.ts b/src/blob/errors/StorageErrorFactory.ts
index 3f394ebf1..38f97dcce 100644
--- a/src/blob/errors/StorageErrorFactory.ts
+++ b/src/blob/errors/StorageErrorFactory.ts
@@ -31,6 +31,17 @@ export default class StorageErrorFactory {
);
}
+ public static getBlobAlreadyExists(
+ contextID: string = DefaultID
+ ): StorageError {
+ return new StorageError(
+ 409,
+ "BlobAlreadyExists",
+ "The specified blob already exists.",
+ contextID
+ );
+ }
+
public static getBlobNotFound(contextID: string = DefaultID): StorageError {
return new StorageError(
404,
@@ -160,6 +171,17 @@ export default class StorageErrorFactory {
);
}
+ public static getInvalidBlobOrBlock(
+ contextID: string = DefaultID
+ ): StorageError {
+ return new StorageError(
+ 400,
+ "InvalidBlobOrBlock",
+ "The specified blob or block content is invalid.",
+ contextID
+ );
+ }
+
public static getLeaseAlreadyPresent(
contextID: string = DefaultID
): StorageError {
@@ -354,6 +376,17 @@ export default class StorageErrorFactory {
);
}
+ public static getMultipleConditionHeadersNotSupported(
+ contextID: string
+ ): StorageError {
+ return new StorageError(
+ 400,
+ "MultipleConditionHeadersNotSupported",
+ "Multiple condition headers are not supported.",
+ contextID
+ );
+ }
+
public static getBlobSnapshotsPresent_hassnapshot(
contextID: string
): StorageError {
@@ -478,6 +511,44 @@ export default class StorageErrorFactory {
);
}
+ public static getConditionNotMet(contextID: string): StorageError {
+ return new StorageError(
+ 412,
+ "ConditionNotMet",
+ "The condition specified using HTTP conditional header(s) is not met.",
+ contextID
+ );
+ }
+
+ public static getSequenceNumberConditionNotMet(
+ contextID: string
+ ): StorageError {
+ return new StorageError(
+ 412,
+ "SequenceNumberConditionNotMet",
+ "The condition specified using HTTP conditional header(s) is not met.",
+ contextID
+ );
+ }
+
+ public static getNotModified(contextID: string): StorageError {
+ return new StorageError(
+ 304,
+ "ConditionNotMet",
+ "The condition specified using HTTP conditional header(s) is not met.",
+ contextID
+ );
+ }
+
+ public static getUnsatisfiableCondition(contextID: string): StorageError {
+ return new StorageError(
+ 400,
+ "UnsatisfiableCondition",
+ "The request includes an unsatisfiable condition for this operation.",
+ contextID
+ );
+ }
+
public static getInvalidHeaderValue(
contextID: string = "",
additionalMessages?: { [key: string]: string }
diff --git a/src/blob/generated/ExpressRequestAdapter.ts b/src/blob/generated/ExpressRequestAdapter.ts
index dced6a1ec..c37abcb31 100644
--- a/src/blob/generated/ExpressRequestAdapter.ts
+++ b/src/blob/generated/ExpressRequestAdapter.ts
@@ -43,6 +43,10 @@ export default class ExpressRequestAdapter implements IRequest {
return this.req.headers;
}
+ public getRawHeaders(): string[] {
+ return this.req.rawHeaders;
+ }
+
public getQuery(key: string): string | undefined {
return this.req.query[key];
}
diff --git a/src/blob/generated/IRequest.ts b/src/blob/generated/IRequest.ts
index 0fb3060ea..df39dd0ad 100644
--- a/src/blob/generated/IRequest.ts
+++ b/src/blob/generated/IRequest.ts
@@ -19,6 +19,7 @@ export default interface IRequest {
getBody(): string | undefined;
getHeader(field: string): string | undefined;
getHeaders(): { [header: string]: string | string[] | undefined };
+ getRawHeaders(): string[];
getQuery(key: string): string | undefined;
getProtocol(): string;
}
diff --git a/src/blob/handlers/BlobHandler.ts b/src/blob/handlers/BlobHandler.ts
index 7f2738579..5d38f9527 100644
--- a/src/blob/handlers/BlobHandler.ts
+++ b/src/blob/handlers/BlobHandler.ts
@@ -1,6 +1,7 @@
import { URL } from "url";
import IExtentStore from "../../common/persistence/IExtentStore";
+import { convertRawHeadersToMetadata } from "../../common/utils/utils";
import BlobStorageContext from "../context/BlobStorageContext";
import NotImplementedError from "../errors/NotImplementedError";
import StorageErrorFactory from "../errors/StorageErrorFactory";
@@ -15,7 +16,8 @@ import IBlobMetadataStore, {
import {
BLOB_API_VERSION,
EMULATOR_ACCOUNT_KIND,
- EMULATOR_ACCOUNT_SKUNAME
+ EMULATOR_ACCOUNT_SKUNAME,
+ HeaderConstants
} from "../utils/constants";
import {
deserializePageBlobRangeHeader,
@@ -96,7 +98,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
containerName,
blobName,
options.snapshot,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
if (blob.properties.blobType === Models.BlobType.BlockBlob) {
@@ -133,7 +136,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
container,
blob,
options.snapshot,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
// TODO: Create get metadata specific request in swagger
@@ -236,16 +240,43 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
const account = blobCtx.account!;
const container = blobCtx.container!;
const blob = blobCtx.blob!;
- const res = await this.metadataStore.setBlobHTTPHeaders(
- context,
- account,
- container,
- blob,
- options.leaseAccessConditions,
- options.blobHTTPHeaders
+
+ let res;
+
+ // Workaround for https://github.com/Azure/Azurite/issues/332
+ const sequenceNumberAction = context.request!.getHeader(
+ HeaderConstants.X_MS_SEQUENCE_NUMBER_ACTION
+ );
+ const sequenceNumber = context.request!.getHeader(
+ HeaderConstants.X_MS_BLOB_SEQUENCE_NUMBER
);
+ if (sequenceNumberAction !== undefined) {
+ this.logger.verbose(
+ "BlobHandler:setHTTPHeaders() Redirect to updateSequenceNumber...",
+ context.contextId
+ );
+ res = await this.metadataStore.updateSequenceNumber(
+ context,
+ account,
+ container,
+ blob,
+ sequenceNumberAction.toLowerCase() as Models.SequenceNumberActionType,
+ sequenceNumber === undefined ? undefined : parseInt(sequenceNumber, 10),
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
+ );
+ } else {
+ res = await this.metadataStore.setBlobHTTPHeaders(
+ context,
+ account,
+ container,
+ blob,
+ options.leaseAccessConditions,
+ options.blobHTTPHeaders,
+ options.modifiedAccessConditions
+ );
+ }
- // ToDo: return correct headers and test for these.
const response: Models.BlobSetHTTPHeadersResponse = {
statusCode: 200,
eTag: res.etag,
@@ -276,13 +307,20 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
const account = blobCtx.account!;
const container = blobCtx.container!;
const blob = blobCtx.blob!;
+
+ // Preserve metadata key case
+ const metadata = convertRawHeadersToMetadata(
+ blobCtx.request!.getRawHeaders()
+ );
+
const res = await this.metadataStore.setBlobMetadata(
context,
account,
container,
blob,
options.leaseAccessConditions,
- options.metadata
+ metadata,
+ options.modifiedAccessConditions
);
// ToDo: return correct headers and test for these.
@@ -331,7 +369,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
container,
blob,
options.duration!,
- options.proposedLeaseId
+ options.proposedLeaseId,
+ options
);
const response: Models.BlobAcquireLeaseResponse = {
@@ -371,7 +410,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
account,
container,
blob,
- leaseId
+ leaseId,
+ options
);
const response: Models.BlobReleaseLeaseResponse = {
@@ -410,7 +450,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
account,
container,
blob,
- leaseId
+ leaseId,
+ options
);
const response: Models.BlobRenewLeaseResponse = {
@@ -453,7 +494,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
container,
blob,
leaseId,
- proposedLeaseId
+ proposedLeaseId,
+ options
);
const response: Models.BlobChangeLeaseResponse = {
@@ -491,7 +533,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
account,
container,
blob,
- options.breakPeriod
+ options.breakPeriod,
+ options
);
const response: Models.BlobBreakLeaseResponse = {
@@ -526,6 +569,12 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
const account = blobCtx.account!;
const container = blobCtx.container!;
const blob = blobCtx.blob!;
+
+ // Preserve metadata key case
+ const metadata = convertRawHeadersToMetadata(
+ blobCtx.request!.getRawHeaders()
+ );
+
const res = await this.metadataStore.createSnapshot(
context,
account,
@@ -534,7 +583,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
options.leaseAccessConditions,
!options.metadata || JSON.stringify(options.metadata) === "{}"
? undefined
- : options.metadata
+ : metadata,
+ options.modifiedAccessConditions
);
const response: Models.BlobCreateSnapshotResponse = {
@@ -576,7 +626,7 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
sourceAccount,
sourceContainer,
sourceBlob
- ] = extractStoragePartsFromPath(url.pathname);
+ ] = extractStoragePartsFromPath(url.hostname, url.pathname);
const snapshot = url.searchParams.get("snapshot") || "";
if (
@@ -588,6 +638,11 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
throw StorageErrorFactory.getBlobNotFound(context.contextId!);
}
+ // Preserve metadata key case
+ const metadata = convertRawHeadersToMetadata(
+ blobCtx.request!.getRawHeaders()
+ );
+
const res = await this.metadataStore.startCopyFromURL(
context,
{
@@ -598,9 +653,9 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
},
{ account, container, blob },
copySource,
- options.metadata,
+ metadata,
options.tier,
- options.leaseAccessConditions
+ options
);
const response: Models.BlobStartCopyFromURLResponse = {
diff --git a/src/blob/handlers/BlockBlobHandler.ts b/src/blob/handlers/BlockBlobHandler.ts
index 0953b43fa..55157bc44 100644
--- a/src/blob/handlers/BlockBlobHandler.ts
+++ b/src/blob/handlers/BlockBlobHandler.ts
@@ -1,3 +1,4 @@
+import { convertRawHeadersToMetadata } from "../../common/utils/utils";
import BlobStorageContext from "../context/BlobStorageContext";
import NotImplementedError from "../errors/NotImplementedError";
import StorageErrorFactory from "../errors/StorageErrorFactory";
@@ -89,7 +90,8 @@ export default class BlockBlobHandler extends BaseHandler
const blob: BlobModel = {
deleted: false,
- metadata: options.metadata,
+ // Preserve metadata key case
+ metadata: convertRawHeadersToMetadata(blobCtx.request!.getRawHeaders()),
accountName,
containerName,
name: blobName,
@@ -131,7 +133,8 @@ export default class BlockBlobHandler extends BaseHandler
await this.metadataStore.createBlob(
context,
blob,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
const response: Models.BlockBlobUploadResponse = {
@@ -284,7 +287,10 @@ export default class BlockBlobHandler extends BaseHandler
};
blob.properties.blobType = Models.BlobType.BlockBlob;
- blob.metadata = options.metadata;
+ blob.metadata = convertRawHeadersToMetadata(
+ // Preserve metadata key case
+ blobCtx.request!.getRawHeaders()
+ );
blob.properties.accessTier = Models.AccessTier.Hot;
blob.properties.cacheControl = options.blobHTTPHeaders.blobCacheControl;
blob.properties.contentType = contentType;
@@ -313,7 +319,8 @@ export default class BlockBlobHandler extends BaseHandler
context,
blob,
commitBlockList,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
const contentMD5 = await getMD5FromString(rawBody);
@@ -352,6 +359,8 @@ export default class BlockBlobHandler extends BaseHandler
);
// TODO: Create uncommitted blockblob when stage block
+ // TODO: Conditional headers support?
+
res.properties = res.properties || {};
const response: Models.BlockBlobGetBlockListResponse = {
statusCode: 200,
diff --git a/src/blob/handlers/ContainerHandler.ts b/src/blob/handlers/ContainerHandler.ts
index 6486997f3..d2a729914 100644
--- a/src/blob/handlers/ContainerHandler.ts
+++ b/src/blob/handlers/ContainerHandler.ts
@@ -1,3 +1,4 @@
+import { convertRawHeadersToMetadata } from "../../common/utils/utils";
import BlobStorageContext from "../context/BlobStorageContext";
import * as Models from "../generated/artifacts/models";
import Context from "../generated/Context";
@@ -38,10 +39,15 @@ export default class ContainerHandler extends BaseHandler
const lastModified = blobCtx.startTime!;
const etag = newEtag();
+ // Preserve metadata key case
+ const metadata = convertRawHeadersToMetadata(
+ blobCtx.request!.getRawHeaders()
+ );
+
await this.metadataStore.createContainer(context, {
accountName,
name: containerName,
- metadata: options.metadata,
+ metadata,
properties: {
etag,
lastModified,
@@ -142,7 +148,7 @@ export default class ContainerHandler extends BaseHandler
context,
accountName,
containerName,
- options.leaseAccessConditions
+ options
);
const response: Models.ContainerDeleteResponse = {
@@ -174,14 +180,20 @@ export default class ContainerHandler extends BaseHandler
const date = blobCtx.startTime!;
const eTag = newEtag();
+ // Preserve metadata key case
+ const metadata = convertRawHeadersToMetadata(
+ blobCtx.request!.getRawHeaders()
+ );
+
await this.metadataStore.setContainerMetadata(
context,
accountName,
containerName,
date,
eTag,
- options.metadata,
- options.leaseAccessConditions
+ metadata,
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
const response: Models.ContainerSetMetadataResponse = {
@@ -271,7 +283,8 @@ export default class ContainerHandler extends BaseHandler
etag: eTag,
publicAccess: options.access,
containerAcl: options.containerAcl,
- leaseAccessConditions: options.leaseAccessConditions
+ leaseAccessConditions: options.leaseAccessConditions,
+ modifiedAccessConditions: options.modifiedAccessConditions
}
);
@@ -351,7 +364,8 @@ export default class ContainerHandler extends BaseHandler
context,
accountName,
containerName,
- leaseId
+ leaseId,
+ options
);
const response: Models.ContainerReleaseLeaseResponse = {
@@ -391,7 +405,8 @@ export default class ContainerHandler extends BaseHandler
context,
accountName,
containerName,
- leaseId
+ leaseId,
+ options
);
const response: Models.ContainerRenewLeaseResponse = {
@@ -430,7 +445,8 @@ export default class ContainerHandler extends BaseHandler
context,
accountName,
containerName,
- options.breakPeriod
+ options.breakPeriod,
+ options
);
const response: Models.ContainerBreakLeaseResponse = {
@@ -474,7 +490,8 @@ export default class ContainerHandler extends BaseHandler
accountName,
containerName,
leaseId,
- proposedLeaseId
+ proposedLeaseId,
+ options
);
const response: Models.ContainerChangeLeaseResponse = {
diff --git a/src/blob/handlers/PageBlobHandler.ts b/src/blob/handlers/PageBlobHandler.ts
index 85f6c87df..1d6cf8e60 100644
--- a/src/blob/handlers/PageBlobHandler.ts
+++ b/src/blob/handlers/PageBlobHandler.ts
@@ -1,4 +1,5 @@
import IExtentStore from "../../common/persistence/IExtentStore";
+import { convertRawHeadersToMetadata } from "../../common/utils/utils";
import BlobStorageContext from "../context/BlobStorageContext";
import NotImplementedError from "../errors/NotImplementedError";
import StorageErrorFactory from "../errors/StorageErrorFactory";
@@ -96,10 +97,15 @@ export default class PageBlobHandler extends BaseHandler
// );
// }
+ // Preserve metadata key case
+ const metadata = convertRawHeadersToMetadata(
+ blobCtx.request!.getRawHeaders()
+ );
+
const etag = newEtag();
const blob: BlobModel = {
deleted: false,
- metadata: options.metadata,
+ metadata,
accountName,
containerName,
name: blobName,
@@ -114,7 +120,9 @@ export default class PageBlobHandler extends BaseHandler
contentMD5: options.blobHTTPHeaders.blobContentMD5,
contentDisposition: options.blobHTTPHeaders.blobContentDisposition,
cacheControl: options.blobHTTPHeaders.blobCacheControl,
- blobSequenceNumber: options.blobSequenceNumber,
+ blobSequenceNumber: options.blobSequenceNumber
+ ? options.blobSequenceNumber
+ : 0,
blobType: Models.BlobType.PageBlob,
leaseStatus: Models.LeaseStatusType.Unlocked,
leaseState: Models.LeaseStateType.Available,
@@ -135,7 +143,8 @@ export default class PageBlobHandler extends BaseHandler
await this.metadataStore.createBlob(
context,
blob,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
const response: Models.PageBlobCreateResponse = {
@@ -226,7 +235,9 @@ export default class PageBlobHandler extends BaseHandler
start,
end,
persistency,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions,
+ options.sequenceNumberAccessConditions
);
const response: Models.PageBlobUploadPagesResponse = {
@@ -295,7 +306,9 @@ export default class PageBlobHandler extends BaseHandler
blob,
start,
end,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions,
+ options.sequenceNumberAccessConditions
);
const response: Models.PageBlobClearPagesResponse = {
@@ -329,7 +342,8 @@ export default class PageBlobHandler extends BaseHandler
containerName,
blobName,
options.snapshot,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
if (blob.properties.blobType !== Models.BlobType.PageBlob) {
@@ -401,7 +415,8 @@ export default class PageBlobHandler extends BaseHandler
containerName,
blobName,
blobContentLength,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
const response: Models.PageBlobResizeResponse = {
@@ -436,7 +451,8 @@ export default class PageBlobHandler extends BaseHandler
blobName,
sequenceNumberAction,
options.blobSequenceNumber,
- options.leaseAccessConditions
+ options.leaseAccessConditions,
+ options.modifiedAccessConditions
);
const response: Models.PageBlobUpdateSequenceNumberResponse = {
diff --git a/src/blob/middlewares/StrictModelMiddlewareFactory.ts b/src/blob/middlewares/StrictModelMiddlewareFactory.ts
index bd205c85d..9551627bf 100644
--- a/src/blob/middlewares/StrictModelMiddlewareFactory.ts
+++ b/src/blob/middlewares/StrictModelMiddlewareFactory.ts
@@ -20,17 +20,6 @@ export const UnsupportedHeadersBlocker: StrictModelRequestValidator = async (
logger: ILogger
): Promise => {
const UnsupportedHeaderKeys = [
- HeaderConstants.IF_MATCH,
- HeaderConstants.IF_NONE_MATCH,
- HeaderConstants.IF_MODIFIED_SINCE,
- HeaderConstants.IF_UNMODIFIED_SINCE,
- HeaderConstants.SOURCE_IF_MATCH,
- HeaderConstants.SOURCE_IF_MODIFIED_SINCE,
- HeaderConstants.SOURCE_IF_NONE_MATCH,
- HeaderConstants.SOURCE_IF_UNMODIFIED_SINCE,
- HeaderConstants.X_MS_IF_SEQUENCE_NUMBER_LE,
- HeaderConstants.X_MS_IF_SEQUENCE_NUMBER_LT,
- HeaderConstants.X_MS_IF_SEQUENCE_NUMBER_EQ,
HeaderConstants.X_MS_BLOB_CONDITION_MAXSIZE,
HeaderConstants.X_MS_BLOB_CONDITION_APPENDPOS,
HeaderConstants.X_MS_CONTENT_CRC64,
diff --git a/src/blob/middlewares/blobStorageContext.middleware.ts b/src/blob/middlewares/blobStorageContext.middleware.ts
index 7985c99e0..e56fe2802 100644
--- a/src/blob/middlewares/blobStorageContext.middleware.ts
+++ b/src/blob/middlewares/blobStorageContext.middleware.ts
@@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from "express";
import uuid from "uuid/v4";
import logger from "../../common/Logger";
+import { PRODUCTION_STYLE_URL_HOSTNAME } from "../../common/utils/constants";
import { checkApiVersion } from "../../common/utils/utils";
import BlobStorageContext from "../context/BlobStorageContext";
import StorageErrorFactory from "../errors/StorageErrorFactory";
@@ -52,6 +53,7 @@ export default function blobStorageContextMiddleware(
);
const [account, container, blob, isSecondary] = extractStoragePartsFromPath(
+ req.hostname,
req.path
);
@@ -60,7 +62,8 @@ export default function blobStorageContextMiddleware(
blobContext.blob = blob;
blobContext.isSecondary = isSecondary;
- // Emulator's URL pattern is like http://hostname:port/account/container
+ // Emulator's URL pattern is like http://hostname[:port]/account/container
+ // (or, alternatively, http[s]://account.localhost[:port]/container)
// Create a router to exclude account name from req.path, as url path in swagger doesn't include account
// Exclude account name from req.path for dispatchMiddleware
blobContext.dispatchPattern = container
@@ -104,6 +107,7 @@ export default function blobStorageContextMiddleware(
* @returns {([string | undefined, string | undefined, string | undefined, boolean | undefined])}
*/
export function extractStoragePartsFromPath(
+ hostname: string,
path: string
): [
string | undefined,
@@ -123,10 +127,18 @@ export function extractStoragePartsFromPath(
const parts = normalizedPath.split("/");
- account = parts[0];
- container = parts[1];
+ let urlPartIndex = 0;
+ if (hostname.endsWith(PRODUCTION_STYLE_URL_HOSTNAME)) {
+ account = hostname.substring(
+ 0,
+ hostname.length - PRODUCTION_STYLE_URL_HOSTNAME.length
+ );
+ } else {
+ account = parts[urlPartIndex++];
+ }
+ container = parts[urlPartIndex++];
blob = parts
- .slice(2)
+ .slice(urlPartIndex++)
.join("/")
.replace(/\\/g, "/"); // Azure Storage Server will replace "\" with "/" in the blob names
diff --git a/src/blob/persistence/IBlobMetadataStore.ts b/src/blob/persistence/IBlobMetadataStore.ts
index fc772ca9b..9390f550a 100644
--- a/src/blob/persistence/IBlobMetadataStore.ts
+++ b/src/blob/persistence/IBlobMetadataStore.ts
@@ -61,6 +61,7 @@ interface ISetContainerAccessPolicyOptions {
containerAcl?: Models.SignedIdentifier[];
publicAccess?: Models.PublicAccessType;
leaseAccessConditions?: Models.LeaseAccessConditions;
+ modifiedAccessConditions?: Models.ModifiedAccessConditions;
}
export type SetContainerAccessPolicyOptions = ISetContainerAccessPolicyOptions;
@@ -296,7 +297,7 @@ export interface IBlobMetadataStore
* @param {string} account
* @param {string} container
* @param {Context} context
- * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ContainerDeleteMethodOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -304,18 +305,20 @@ export interface IBlobMetadataStore
context: Context,
account: string,
container: string,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ options?: Models.ContainerDeleteMethodOptionalParams
): Promise;
- /** Set container metadata.
+ /**
+ * Set container metadata.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {Date} lastModified
* @param {string} etag
- * @param {Context} context
* @param {IContainerMetadata} [metadata]
* @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -326,7 +329,8 @@ export interface IBlobMetadataStore
lastModified: Date,
etag: string,
metadata?: IContainerMetadata,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -368,7 +372,7 @@ export interface IBlobMetadataStore
*
* @param {string} account
* @param {string} container
- * @param {Models.ContainerAcquireLeaseOptionalParams} options
+ * @param {Models.ContainerAcquireLeaseOptionalParams} [options]
* @param {Context} context
* @returns {Promise}
* @memberof IBlobMetadataStore
@@ -377,16 +381,17 @@ export interface IBlobMetadataStore
context: Context,
account: string,
container: string,
- options: Models.ContainerAcquireLeaseOptionalParams
+ options?: Models.ContainerAcquireLeaseOptionalParams
): Promise;
/**
- * Release container lease
+ * Release container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} leaseId
- * @param {Context} context
+ * @param {Models.ContainerReleaseLeaseOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -394,16 +399,18 @@ export interface IBlobMetadataStore
context: Context,
account: string,
container: string,
- leaseId: string
+ leaseId: string,
+ options?: Models.ContainerReleaseLeaseOptionalParams
): Promise;
/**
- * Renew container lease
+ * Renew container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} leaseId
- * @param {Context} context
+ * @param {Models.ContainerRenewLeaseOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -411,16 +418,18 @@ export interface IBlobMetadataStore
context: Context,
account: string,
container: string,
- leaseId: string
+ leaseId: string,
+ options?: Models.ContainerRenewLeaseOptionalParams
): Promise;
/**
- * Break container lease
+ * Break container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {(number | undefined)} breakPeriod
- * @param {Context} context
+ * @param {Models.ContainerBreakLeaseOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -428,17 +437,19 @@ export interface IBlobMetadataStore
context: Context,
account: string,
container: string,
- breakPeriod: number | undefined
+ breakPeriod: number | undefined,
+ options?: Models.ContainerBreakLeaseOptionalParams
): Promise;
/**
- * Change container lease
+ * Change container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} leaseId
* @param {string} proposedLeaseId
- * @param {Context} context
+ * @param {Models.ContainerChangeLeaseOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -447,7 +458,8 @@ export interface IBlobMetadataStore
account: string,
container: string,
leaseId: string,
- proposedLeaseId: string
+ proposedLeaseId: string,
+ options?: Models.ContainerChangeLeaseOptionalParams
): Promise;
/**
@@ -488,13 +500,15 @@ export interface IBlobMetadataStore
* @param {Context} context
* @param {BlobModel} blob
* @param {Models.LeaseAccessConditions} [leaseAccessConditions] Optional. Will validate lease if provided
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
createBlob(
context: Context,
blob: BlobModel,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -505,6 +519,8 @@ export interface IBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {Models.LeaseAccessConditions} [leaseAccessConditions] Optional. Will validate lease if provided
+ * @param {Models.BlobMetadata} [metadata]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -514,7 +530,8 @@ export interface IBlobMetadataStore
container: string,
blob: string,
leaseAccessConditions?: Models.LeaseAccessConditions,
- metadata?: Models.BlobMetadata
+ metadata?: Models.BlobMetadata,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -527,6 +544,7 @@ export interface IBlobMetadataStore
* @param {string} blob
* @param {(string | undefined)} snapshot
* @param {Models.LeaseAccessConditions} [leaseAccessConditions] Optional. Will validate lease if provided
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -536,7 +554,8 @@ export interface IBlobMetadataStore
container: string,
blob: string,
snapshot: string | undefined,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -548,6 +567,7 @@ export interface IBlobMetadataStore
* @param {string} blob
* @param {(string | undefined)} snapshot
* @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -557,7 +577,8 @@ export interface IBlobMetadataStore
container: string,
blob: string,
snapshot: string | undefined,
- leaseAccessConditions: Models.LeaseAccessConditions | undefined
+ leaseAccessConditions: Models.LeaseAccessConditions | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -588,6 +609,7 @@ export interface IBlobMetadataStore
* @param {string} blob
* @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
* @param {(Models.BlobHTTPHeaders | undefined)} blobHTTPHeaders
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -597,7 +619,8 @@ export interface IBlobMetadataStore
container: string,
blob: string,
leaseAccessConditions: Models.LeaseAccessConditions | undefined,
- blobHTTPHeaders: Models.BlobHTTPHeaders | undefined
+ blobHTTPHeaders: Models.BlobHTTPHeaders | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -609,6 +632,7 @@ export interface IBlobMetadataStore
* @param {string} blob
* @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
* @param {(Models.BlobMetadata | undefined)} metadata
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -618,7 +642,8 @@ export interface IBlobMetadataStore
container: string,
blob: string,
leaseAccessConditions: Models.LeaseAccessConditions | undefined,
- metadata: Models.BlobMetadata | undefined
+ metadata: Models.BlobMetadata | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -630,6 +655,7 @@ export interface IBlobMetadataStore
* @param {string} blob
* @param {number} duration
* @param {string} [proposedLeaseId]
+ * @param {Models.BlobAcquireLeaseOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -639,7 +665,8 @@ export interface IBlobMetadataStore
container: string,
blob: string,
duration: number,
- proposedLeaseId?: string
+ proposedLeaseId?: string,
+ options?: Models.BlobAcquireLeaseOptionalParams
): Promise;
/**
@@ -650,6 +677,7 @@ export interface IBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {string} leaseId
+ * @param {Models.BlobReleaseLeaseOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -658,7 +686,8 @@ export interface IBlobMetadataStore
account: string,
container: string,
blob: string,
- leaseId: string
+ leaseId: string,
+ options?: Models.BlobReleaseLeaseOptionalParams
): Promise;
/**
@@ -669,6 +698,7 @@ export interface IBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {string} leaseId
+ * @param {Models.BlobRenewLeaseOptionalParams} [options]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -677,7 +707,8 @@ export interface IBlobMetadataStore
account: string,
container: string,
blob: string,
- leaseId: string
+ leaseId: string,
+ options?: Models.BlobRenewLeaseOptionalParams
): Promise;
/**
@@ -689,6 +720,7 @@ export interface IBlobMetadataStore
* @param {string} blob
* @param {string} leaseId
* @param {string} proposedLeaseId
+ * @param {Models.BlobChangeLeaseOptionalParams} [option]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -698,17 +730,19 @@ export interface IBlobMetadataStore
container: string,
blob: string,
leaseId: string,
- proposedLeaseId: string
+ proposedLeaseId: string,
+ option?: Models.BlobChangeLeaseOptionalParams
): Promise;
/**
- * Break blob lease
+ * Break blob lease.
*
* @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} blob
* @param {number} [breakPeriod]
+ * @param {Models.BlobBreakLeaseOptionalParams} [option]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -717,7 +751,8 @@ export interface IBlobMetadataStore
account: string,
container: string,
blob: string,
- breakPeriod?: number
+ breakPeriod?: number,
+ option?: Models.BlobBreakLeaseOptionalParams
): Promise;
/**
@@ -769,7 +804,7 @@ export interface IBlobMetadataStore
* @param {string} copySource
* @param {(Models.BlobMetadata | undefined)} metadata
* @param {(Models.AccessTier | undefined)} tier
- * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
+ * @param {Models.BlobStartCopyFromURLOptionalParams} [leaseAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -780,7 +815,7 @@ export interface IBlobMetadataStore
copySource: string,
metadata: Models.BlobMetadata | undefined,
tier: Models.AccessTier | undefined,
- leaseAccessConditions: Models.LeaseAccessConditions | undefined
+ leaseAccessConditions?: Models.BlobStartCopyFromURLOptionalParams
): Promise;
/**
@@ -822,10 +857,11 @@ export interface IBlobMetadataStore
/**
* Commit block list for a blob.
*
+ * @param {Context} context
* @param {BlobModel} blob
* @param {{ blockName: string; blockCommitType: string }[]} blockList
- * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
- * @param {Context} context
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -833,7 +869,8 @@ export interface IBlobMetadataStore
context: Context,
blob: BlobModel,
blockList: { blockName: string; blockCommitType: string }[],
- leaseAccessConditions: Models.LeaseAccessConditions | undefined
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
@@ -867,11 +904,14 @@ export interface IBlobMetadataStore
/**
* Upload new pages for page blob.
*
+ * @param {Context} context
* @param {BlobModel} blob
* @param {number} start
* @param {number} end
* @param {IExtentChunk} persistency
- * @param {Context} [context]
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
+ * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -881,16 +921,21 @@ export interface IBlobMetadataStore
start: number,
end: number,
persistency: IExtentChunk,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions,
+ sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions
): Promise;
/**
* Clear range for a page blob.
*
+ * @param {Context} context
* @param {BlobModel} blob
* @param {number} start
* @param {number} end
- * @param {Context} [context]
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
+ * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -899,7 +944,9 @@ export interface IBlobMetadataStore
blob: BlobModel,
start: number,
end: number,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions,
+ sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions
): Promise;
/**
@@ -910,6 +957,8 @@ export interface IBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {string} [snapshot]
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -919,17 +968,20 @@ export interface IBlobMetadataStore
container: string,
blob: string,
snapshot?: string,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
* Resize a page blob.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} blob
* @param {number} blobContentLength
- * @param {Context} context
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -939,18 +991,21 @@ export interface IBlobMetadataStore
container: string,
blob: string,
blobContentLength: number,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
* Update the sequence number of a page blob.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} blob
* @param {Models.SequenceNumberActionType} sequenceNumberAction
* @param {(number | undefined)} blobSequenceNumber
- * @param {Context} context
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof IBlobMetadataStore
*/
@@ -961,7 +1016,8 @@ export interface IBlobMetadataStore
blob: string,
sequenceNumberAction: Models.SequenceNumberActionType,
blobSequenceNumber: number | undefined,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise;
/**
diff --git a/src/blob/persistence/LokiBlobMetadataStore.ts b/src/blob/persistence/LokiBlobMetadataStore.ts
index 9f4d4a78b..3a55f3832 100644
--- a/src/blob/persistence/LokiBlobMetadataStore.ts
+++ b/src/blob/persistence/LokiBlobMetadataStore.ts
@@ -7,9 +7,13 @@ import {
convertDateTimeStringMsTo7Digital,
rimrafAsync
} from "../../common/utils/utils";
+import { validateReadConditions } from "../conditions/ReadConditionalHeadersValidator";
+import {
+ validateSequenceNumberWriteConditions,
+ validateWriteConditions
+} from "../conditions/WriteConditionalHeadersValidator";
import StorageErrorFactory from "../errors/StorageErrorFactory";
import * as Models from "../generated/artifacts/models";
-import { LeaseStatusType } from "../generated/artifacts/models";
import Context from "../generated/Context";
import PageBlobRangesManager from "../handlers/PageBlobRangesManager";
import BlobLeaseAdapter from "../lease/BlobLeaseAdapter";
@@ -428,7 +432,7 @@ export default class LokiBlobMetadataStore
* @param {string} account
* @param {string} container
* @param {Context} context
- * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ContainerDeleteMethodOptionalParams} [options]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -436,16 +440,23 @@ export default class LokiBlobMetadataStore
context: Context,
account: string,
container: string,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ options: Models.ContainerDeleteMethodOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
const doc = await this.getContainerWithLeaseUpdated(
account,
container,
- context
+ context,
+ false
);
- new ContainerDeleteLeaseValidator(leaseAccessConditions).validate(
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
+
+ new ContainerDeleteLeaseValidator(options.leaseAccessConditions).validate(
new ContainerLeaseAdapter(doc),
context
);
@@ -468,13 +479,14 @@ export default class LokiBlobMetadataStore
/**
* Set container metadata.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {Date} lastModified
* @param {string} etag
- * @param {Context} context
* @param {IContainerMetadata} [metadata]
* @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -485,15 +497,23 @@ export default class LokiBlobMetadataStore
lastModified: Date,
etag: string,
metadata?: IContainerMetadata,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
const doc = await this.getContainerWithLeaseUpdated(
account,
container,
- context
+ context,
+ false
);
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
+
new ContainerReadLeaseValidator(leaseAccessConditions).validate(
new ContainerLeaseAdapter(doc),
context
@@ -561,9 +581,16 @@ export default class LokiBlobMetadataStore
const doc = await this.getContainerWithLeaseUpdated(
account,
container,
- context
+ context,
+ false
);
+ validateWriteConditions(context, setAclModel.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
+
new ContainerReadLeaseValidator(setAclModel.leaseAccessConditions).validate(
new ContainerLeaseAdapter(doc),
context
@@ -594,7 +621,13 @@ export default class LokiBlobMetadataStore
options: Models.ContainerAcquireLeaseOptionalParams
): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
- const doc = await this.getContainer(account, container, context);
+ const doc = await this.getContainer(account, container, context, false);
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context)
.acquire(options.duration!, options.proposedLeaseId)
@@ -608,10 +641,11 @@ export default class LokiBlobMetadataStore
/**
* Release container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} leaseId
- * @param {Context} context
+ * @param {Models.ContainerReleaseLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -619,10 +653,17 @@ export default class LokiBlobMetadataStore
context: Context,
account: string,
container: string,
- leaseId: string
+ leaseId: string,
+ options: Models.ContainerReleaseLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
- const doc = await this.getContainer(account, container, context);
+ const doc = await this.getContainer(account, container, context, false);
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context)
.release(leaseId)
@@ -636,10 +677,11 @@ export default class LokiBlobMetadataStore
/**
* Renew container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} leaseId
- * @param {Context} context
+ * @param {Models.ContainerRenewLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -647,10 +689,17 @@ export default class LokiBlobMetadataStore
context: Context,
account: string,
container: string,
- leaseId: string
+ leaseId: string,
+ options: Models.ContainerRenewLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
- const doc = await this.getContainer(account, container, context);
+ const doc = await this.getContainer(account, container, context, false);
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context)
.renew(leaseId)
@@ -664,10 +713,11 @@ export default class LokiBlobMetadataStore
/**
* Break container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {(number | undefined)} breakPeriod
- * @param {Context} context
+ * @param {Models.ContainerBreakLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -675,10 +725,17 @@ export default class LokiBlobMetadataStore
context: Context,
account: string,
container: string,
- breakPeriod: number | undefined
+ breakPeriod: number | undefined,
+ options: Models.ContainerBreakLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
- const doc = await this.getContainer(account, container, context);
+ const doc = await this.getContainer(account, container, context, false);
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context)
.break(breakPeriod)
@@ -700,11 +757,12 @@ export default class LokiBlobMetadataStore
/**
* Change container lease.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} leaseId
* @param {string} proposedLeaseId
- * @param {Context} context
+ * @param {Models.ContainerChangeLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -713,10 +771,17 @@ export default class LokiBlobMetadataStore
account: string,
container: string,
leaseId: string,
- proposedLeaseId: string
+ proposedLeaseId: string,
+ options: Models.ContainerChangeLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
- const doc = await this.getContainer(account, container, context);
+ const doc = await this.getContainer(account, container, context, false);
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context)
.change(leaseId, proposedLeaseId)
@@ -880,13 +945,15 @@ export default class LokiBlobMetadataStore
* @param {Context} context
* @param {BlobModel} blob
* @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
public async createBlob(
context: Context,
blob: BlobModel,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
await this.checkContainerExist(
context,
@@ -901,6 +968,17 @@ export default class LokiBlobMetadataStore
snapshot: blob.snapshot
});
+ validateWriteConditions(context, modifiedAccessConditions, blobDoc);
+
+ // Create if not exists
+ if (
+ modifiedAccessConditions &&
+ modifiedAccessConditions.ifNoneMatch === "*" &&
+ blobDoc
+ ) {
+ throw StorageErrorFactory.getBlobAlreadyExists(context.contextId);
+ }
+
if (blobDoc) {
LeaseFactory.createLeaseState(
new BlobLeaseAdapter(blobDoc),
@@ -936,7 +1014,8 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
leaseAccessConditions?: Models.LeaseAccessConditions,
- metadata?: Models.BlobMetadata
+ metadata?: Models.BlobMetadata,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -944,9 +1023,17 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
+ context,
+ false,
+ true
);
+ validateReadConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
new BlobReadLeaseValidator(leaseAccessConditions).validate(
new BlobLeaseAdapter(doc),
context
@@ -1003,6 +1090,7 @@ export default class LokiBlobMetadataStore
* @param {string} blob
* @param {string} [snapshot=""]
* @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1012,16 +1100,25 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
snapshot: string = "",
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const doc = await this.getBlobWithLeaseUpdated(
account,
container,
blob,
snapshot,
- context
+ context,
+ false,
+ true
);
+ validateReadConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
new BlobReadLeaseValidator(leaseAccessConditions).validate(
new BlobLeaseAdapter(doc),
context
@@ -1068,13 +1165,15 @@ export default class LokiBlobMetadataStore
}
/**
- * Get blob properties
+ * Get blob properties.
*
* @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} blob
* @param {string} [snapshot=""]
+ * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1084,16 +1183,26 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
snapshot: string = "",
- leaseAccessConditions: Models.LeaseAccessConditions | undefined
+ leaseAccessConditions: Models.LeaseAccessConditions | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const doc = await this.getBlobWithLeaseUpdated(
account,
container,
blob,
snapshot,
- context
+ context,
+ false,
+ true
);
+ validateReadConditions(context, modifiedAccessConditions, doc);
+
+ // When block blob don't have commited block, should return 404
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
new BlobReadLeaseValidator(leaseAccessConditions).validate(
new BlobLeaseAdapter(doc),
context
@@ -1128,8 +1237,16 @@ export default class LokiBlobMetadataStore
container,
blob,
options.snapshot,
- context
+ context,
+ false
);
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
const againstBaseBlob = doc.snapshot === "";
// Check bad requests
@@ -1207,9 +1324,9 @@ export default class LokiBlobMetadataStore
* @param {string} account
* @param {string} container
* @param {string} blob
- * @param {(string | undefined)} snapshot
* @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
* @param {(Models.BlobHTTPHeaders | undefined)} blobHTTPHeaders
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1219,7 +1336,8 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
leaseAccessConditions: Models.LeaseAccessConditions | undefined,
- blobHTTPHeaders: Models.BlobHTTPHeaders | undefined
+ blobHTTPHeaders: Models.BlobHTTPHeaders | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1227,9 +1345,17 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
+ context,
+ false,
+ true
);
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
const lease = new BlobLeaseAdapter(doc);
new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context);
@@ -1248,12 +1374,10 @@ export default class LokiBlobMetadataStore
blobProps.contentEncoding = blobHeaders.blobContentEncoding;
blobProps.contentLanguage = blobHeaders.blobContentLanguage;
blobProps.contentDisposition = blobHeaders.blobContentDisposition;
- blobProps.lastModified = context.startTime
- ? context.startTime
- : new Date();
}
doc.properties = blobProps;
doc.properties.etag = newEtag();
+ blobProps.lastModified = context.startTime ? context.startTime : new Date();
new BlobWriteLeaseSyncer(doc).sync(lease);
@@ -1270,6 +1394,7 @@ export default class LokiBlobMetadataStore
* @param {string} blob
* @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
* @param {(Models.BlobMetadata | undefined)} metadata
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1279,7 +1404,8 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
leaseAccessConditions: Models.LeaseAccessConditions | undefined,
- metadata: Models.BlobMetadata | undefined
+ metadata: Models.BlobMetadata | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1287,14 +1413,23 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
+ context,
+ false,
+ true
);
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
const lease = new BlobLeaseAdapter(doc);
new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context);
new BlobWriteLeaseSyncer(doc).sync(lease);
doc.metadata = metadata;
doc.properties.etag = newEtag();
+ doc.properties.lastModified = context.startTime || new Date();
coll.update(doc);
return doc.properties;
}
@@ -1308,6 +1443,7 @@ export default class LokiBlobMetadataStore
* @param {string} blob
* @param {number} duration
* @param {string} [proposedLeaseId]
+ * @param {Models.BlobAcquireLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1317,7 +1453,8 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
duration: number,
- proposedLeaseId?: string
+ proposedLeaseId?: string,
+ options: Models.BlobAcquireLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1325,11 +1462,19 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
- );
+ context,
+ false
+ ); // This may return an uncommitted blob, or undefined for an unexist blob
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ // Azure Storage allows lease for a uncommitted blob
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
if (doc.snapshot !== "") {
- throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!);
+ throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId);
}
LeaseFactory.createLeaseState(new BlobLeaseAdapter(doc), context)
@@ -1349,6 +1494,7 @@ export default class LokiBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {string} leaseId
+ * @param {Models.BlobReleaseLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1357,7 +1503,8 @@ export default class LokiBlobMetadataStore
account: string,
container: string,
blob: string,
- leaseId: string
+ leaseId: string,
+ options: Models.BlobReleaseLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1365,8 +1512,16 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
- );
+ context,
+ false
+ ); // This may return an uncommitted blob, or undefined for an unexist blob
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ // Azure Storage allows lease for a uncommitted blob
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
if (doc.snapshot !== "") {
throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!);
@@ -1389,6 +1544,7 @@ export default class LokiBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {string} leaseId
+ * @param {Models.BlobRenewLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1397,7 +1553,8 @@ export default class LokiBlobMetadataStore
account: string,
container: string,
blob: string,
- leaseId: string
+ leaseId: string,
+ options: Models.BlobRenewLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1405,8 +1562,16 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
- );
+ context,
+ false
+ ); // This may return an uncommitted blob, or undefined for an unexist blob
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ // Azure Storage allows lease for a uncommitted blob
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
if (doc.snapshot !== "") {
throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!);
@@ -1430,6 +1595,7 @@ export default class LokiBlobMetadataStore
* @param {string} blob
* @param {string} leaseId
* @param {string} proposedLeaseId
+ * @param {Models.BlobChangeLeaseOptionalParams} [option={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1439,7 +1605,8 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
leaseId: string,
- proposedLeaseId: string
+ proposedLeaseId: string,
+ options: Models.BlobChangeLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1447,8 +1614,16 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
- );
+ context,
+ false
+ ); // This may return an uncommitted blob, or undefined for an unexist blob
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ // Azure Storage allows lease for a uncommitted blob
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
if (doc.snapshot !== "") {
throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!);
@@ -1471,6 +1646,7 @@ export default class LokiBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {(number | undefined)} breakPeriod
+ * @param {Models.BlobBreakLeaseOptionalParams} [options={}]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1479,7 +1655,8 @@ export default class LokiBlobMetadataStore
account: string,
container: string,
blob: string,
- breakPeriod: number | undefined
+ breakPeriod: number | undefined,
+ options: Models.BlobBreakLeaseOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1487,8 +1664,16 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
- );
+ context,
+ false
+ ); // This may return an uncommitted blob, or undefined for an unexist blob
+
+ validateWriteConditions(context, options.modifiedAccessConditions, doc);
+
+ // Azure Storage allows lease for a uncommitted blob
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
if (doc.snapshot !== "") {
throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!);
@@ -1579,14 +1764,15 @@ export default class LokiBlobMetadataStore
}
/**
- * start copy from Url
+ * Start copy from Url.
*
* @param {Context} context
* @param {BlobId} source
* @param {BlobId} destination
+ * @param {string} copySource
* @param {(Models.BlobMetadata | undefined)} metadata
* @param {(Models.AccessTier | undefined)} tier
- * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
+ * @param {Models.BlobStartCopyFromURLOptionalParams} [leaseAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1597,7 +1783,7 @@ export default class LokiBlobMetadataStore
copySource: string,
metadata: Models.BlobMetadata | undefined,
tier: Models.AccessTier | undefined,
- leaseAccessConditions: Models.LeaseAccessConditions | undefined
+ options: Models.BlobStartCopyFromURLOptionalParams = {}
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const sourceBlob = await this.getBlobWithLeaseUpdated(
@@ -1605,7 +1791,24 @@ export default class LokiBlobMetadataStore
source.container,
source.blob,
source.snapshot,
- context
+ context,
+ true,
+ true
+ );
+
+ options.sourceModifiedAccessConditions =
+ options.sourceModifiedAccessConditions || {};
+ validateReadConditions(
+ context,
+ {
+ ifModifiedSince:
+ options.sourceModifiedAccessConditions.sourceIfModifiedSince,
+ ifUnmodifiedSince:
+ options.sourceModifiedAccessConditions.sourceIfUnmodifiedSince,
+ ifMatch: options.sourceModifiedAccessConditions.sourceIfMatches,
+ ifNoneMatch: options.sourceModifiedAccessConditions.sourceIfNoneMatch
+ },
+ sourceBlob
);
const destBlob = await this.getBlobWithLeaseUpdated(
@@ -1617,8 +1820,14 @@ export default class LokiBlobMetadataStore
false
);
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ destBlob
+ );
+
if (destBlob) {
- new BlobWriteLeaseValidator(leaseAccessConditions).validate(
+ new BlobWriteLeaseValidator(options.leaseAccessConditions).validate(
new BlobLeaseAdapter(destBlob),
context
);
@@ -1755,7 +1964,9 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
+ context,
+ true,
+ true
);
let responseCode: 200 | 202 = 200;
@@ -1831,6 +2042,8 @@ export default class LokiBlobMetadataStore
name: block.blobName
});
+ let blobExist = false;
+
if (!blobDoc) {
const etag = newEtag();
const newBlob = {
@@ -1853,9 +2066,28 @@ export default class LokiBlobMetadataStore
LeaseFactory.createLeaseState(new BlobLeaseAdapter(blobDoc), context)
.validate(new BlobWriteLeaseValidator(leaseAccessConditions))
.sync(new BlobWriteLeaseSyncer(blobDoc));
+ blobExist = true;
}
const coll = this.db.getCollection(this.BLOCKS_COLLECTION);
+
+ // If the new block ID does not have same length with before uncommited block ID, return failure.
+ if (blobExist) {
+ const existBlockDoc = coll.findOne({
+ accountName: block.accountName,
+ containerName: block.containerName,
+ blobName: block.blobName
+ });
+ if (existBlockDoc) {
+ if (
+ Buffer.from(existBlockDoc.name, "base64").length !==
+ Buffer.from(block.name, "base64").length
+ ) {
+ throw StorageErrorFactory.getInvalidBlobOrBlock(context.contextId);
+ }
+ }
+ }
+
const blockDoc = coll.findOne({
accountName: block.accountName,
containerName: block.containerName,
@@ -1875,10 +2107,11 @@ export default class LokiBlobMetadataStore
/**
* Commit block list for a blob.
*
+ * @param {Context} context
* @param {BlobModel} blob
* @param {{ blockName: string; blockCommitType: string }[]} blockList
- * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions
- * @param {Context} context
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -1886,7 +2119,8 @@ export default class LokiBlobMetadataStore
context: Context,
blob: BlobModel,
blockList: { blockName: string; blockCommitType: string }[],
- leaseAccessConditions: Models.LeaseAccessConditions | undefined
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -1900,6 +2134,18 @@ export default class LokiBlobMetadataStore
false
);
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ // Create if not exists
+ if (
+ modifiedAccessConditions &&
+ modifiedAccessConditions.ifNoneMatch === "*" &&
+ doc &&
+ doc.isCommitted
+ ) {
+ throw StorageErrorFactory.getBlobAlreadyExists(context.contextId);
+ }
+
let lease: ILease | undefined;
if (doc) {
lease = new BlobLeaseAdapter(doc);
@@ -2092,11 +2338,14 @@ export default class LokiBlobMetadataStore
/**
* Upload new pages for page blob.
*
+ * @param {Context} context
* @param {BlobModel} blob
* @param {number} start
* @param {number} end
* @param {IExtentChunk} persistency
- * @param {Context} [context]
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
+ * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -2106,7 +2355,9 @@ export default class LokiBlobMetadataStore
start: number,
end: number,
persistency: IExtentChunk,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions,
+ sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -2114,9 +2365,23 @@ export default class LokiBlobMetadataStore
blob.containerName,
blob.name,
blob.snapshot,
- context!
+ context!,
+ false,
+ true
+ );
+
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ validateSequenceNumberWriteConditions(
+ context,
+ sequenceNumberAccessConditions,
+ doc
);
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
const lease = new BlobLeaseAdapter(doc);
new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context);
@@ -2130,6 +2395,7 @@ export default class LokiBlobMetadataStore
new BlobWriteLeaseSyncer(doc).sync(lease);
doc.properties.etag = newEtag();
+ doc.properties.lastModified = context.startTime || new Date();
coll.update(doc);
@@ -2139,10 +2405,13 @@ export default class LokiBlobMetadataStore
/**
* Clear range for a page blob.
*
+ * @param {Context} context
* @param {BlobModel} blob
* @param {number} start
* @param {number} end
- * @param {Context} [context]
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
+ * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -2151,7 +2420,9 @@ export default class LokiBlobMetadataStore
blob: BlobModel,
start: number,
end: number,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions,
+ sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -2159,9 +2430,23 @@ export default class LokiBlobMetadataStore
blob.containerName,
blob.name,
blob.snapshot,
- context!
+ context!,
+ false,
+ true
+ );
+
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ validateSequenceNumberWriteConditions(
+ context,
+ sequenceNumberAccessConditions,
+ doc
);
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
const lease = new BlobLeaseAdapter(doc);
new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context);
@@ -2175,6 +2460,8 @@ export default class LokiBlobMetadataStore
new BlobWriteLeaseSyncer(doc).sync(lease);
doc.properties.etag = newEtag();
+ doc.properties.lastModified = context.startTime || new Date();
+
coll.update(doc);
return doc.properties;
@@ -2188,6 +2475,8 @@ export default class LokiBlobMetadataStore
* @param {string} container
* @param {string} blob
* @param {string} [snapshot]
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -2197,16 +2486,25 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
snapshot?: string,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const doc = await this.getBlobWithLeaseUpdated(
account,
container,
blob,
snapshot,
- context
+ context,
+ false,
+ true
);
+ validateReadConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
new BlobReadLeaseValidator(leaseAccessConditions).validate(
new BlobLeaseAdapter(doc),
context
@@ -2221,11 +2519,13 @@ export default class LokiBlobMetadataStore
/**
* Resize a page blob.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} blob
* @param {number} blobContentLength
- * @param {Context} context
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -2235,7 +2535,8 @@ export default class LokiBlobMetadataStore
container: string,
blob: string,
blobContentLength: number,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -2243,13 +2544,20 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
+ context,
+ false,
+ true
);
- const requestId = context ? context.contextId : undefined;
+
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
if (doc.properties.blobType !== Models.BlobType.PageBlob) {
throw StorageErrorFactory.getInvalidOperation(
- requestId,
+ context.contextId,
"Resize could only be against a page blob."
);
}
@@ -2268,7 +2576,7 @@ export default class LokiBlobMetadataStore
}
doc.properties.contentLength = blobContentLength;
- doc.properties.lastModified = context.startTime!;
+ doc.properties.lastModified = context.startTime || new Date();
doc.properties.etag = newEtag();
new BlobWriteLeaseSyncer(doc).sync(lease);
@@ -2280,12 +2588,14 @@ export default class LokiBlobMetadataStore
/**
* Update the sequence number of a page blob.
*
+ * @param {Context} context
* @param {string} account
* @param {string} container
* @param {string} blob
* @param {Models.SequenceNumberActionType} sequenceNumberAction
* @param {(number | undefined)} blobSequenceNumber
- * @param {Context} context
+ * @param {Models.LeaseAccessConditions} [leaseAccessConditions]
+ * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
@@ -2296,7 +2606,8 @@ export default class LokiBlobMetadataStore
blob: string,
sequenceNumberAction: Models.SequenceNumberActionType,
blobSequenceNumber: number | undefined,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
const coll = this.db.getCollection(this.BLOBS_COLLECTION);
const doc = await this.getBlobWithLeaseUpdated(
@@ -2304,9 +2615,17 @@ export default class LokiBlobMetadataStore
container,
blob,
undefined,
- context
+ context,
+ false,
+ true
);
+ validateWriteConditions(context, modifiedAccessConditions, doc);
+
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
if (doc.properties.blobType !== Models.BlobType.PageBlob) {
throw StorageErrorFactory.getInvalidOperation(
context.contextId!,
@@ -2360,6 +2679,7 @@ export default class LokiBlobMetadataStore
}
doc.properties.etag = newEtag();
+ doc.properties.lastModified = context.startTime!;
new BlobWriteLeaseSyncer(doc).sync(lease);
coll.update(doc);
@@ -2450,15 +2770,63 @@ export default class LokiBlobMetadataStore
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
+
+ /**
+ * Get a container document from container collection.
+ * Updated lease related properties according to current time.
+ * Will throw ContainerNotFound storage error if container doesn't exist.
+ *
+ * @private
+ * @param {string} account
+ * @param {string} container
+ * @param {Context} context
+ * @returns {Promise}
+ * @memberof LokiBlobMetadataStore
+ */
private async getContainerWithLeaseUpdated(
account: string,
container: string,
- context: Context
- ): Promise {
+ context: Context,
+ forceExist?: true
+ ): Promise;
+
+ /**
+ * Get a container document from container collection.
+ * Updated lease related properties according to current time.
+ * Will NOT throw ContainerNotFound storage error if container doesn't exist.
+ *
+ * @private
+ * @param {string} account
+ * @param {string} container
+ * @param {Context} context
+ * @param {false} forceExist
+ * @returns {(Promise)}
+ * @memberof LokiBlobMetadataStore
+ */
+ private async getContainerWithLeaseUpdated(
+ account: string,
+ container: string,
+ context: Context,
+ forceExist: false
+ ): Promise;
+
+ private async getContainerWithLeaseUpdated(
+ account: string,
+ container: string,
+ context: Context,
+ forceExist?: boolean
+ ): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
const doc = coll.findOne({ accountName: account, name: container });
+
+ if (forceExist === undefined || forceExist === true) {
+ if (!doc) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ }
+ }
+
if (!doc) {
- throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ return undefined;
}
LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context).sync(
@@ -2469,72 +2837,121 @@ export default class LokiBlobMetadataStore
}
/**
- * Get a container document from collections.
+ * Get a container document from Loki collections.
+ * Will throw ContainerNotFound error when container doesn't exist.
*
+ * @private
* @param {string} account
* @param {string} container
* @param {Context} context
+ * @param {true} [forceExist]
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
private async getContainer(
account: string,
container: string,
- context: Context
- ): Promise {
+ context: Context,
+ forceExist?: true
+ ): Promise;
+
+ /**
+ * Get a container document from Loki collections.
+ * Will NOT throw ContainerNotFound error when container doesn't exist.
+ *
+ * @private
+ * @param {string} account
+ * @param {string} container
+ * @param {Context} context
+ * @param {false} forceExist
+ * @returns {Promise}
+ * @memberof LokiBlobMetadataStore
+ */
+ private async getContainer(
+ account: string,
+ container: string,
+ context: Context,
+ forceExist: false
+ ): Promise;
+
+ private async getContainer(
+ account: string,
+ container: string,
+ context: Context,
+ forceExist?: boolean
+ ): Promise {
const coll = this.db.getCollection(this.CONTAINERS_COLLECTION);
const doc = coll.findOne({ accountName: account, name: container });
+
if (!doc) {
- const requestId = context ? context.contextId : undefined;
- throw StorageErrorFactory.getContainerNotFound(requestId);
+ if (forceExist) {
+ throw StorageErrorFactory.getContainerNotFound(context.contextId);
+ } else {
+ return undefined;
+ }
}
return doc;
}
/**
- * Get a blob doc from collections.
+ * Get a blob document model from Loki collection.
+ * Will throw BlobNotFound storage error if blob doesn't exist.
*
* @private
* @param {string} account
* @param {string} container
* @param {string} blob
- * @param {string} snapshot
+ * @param {(string | undefined)} snapshot
* @param {Context} context
+ * @param {undefined} [forceExist]
+ * @param {boolean} [forceCommitted] If true, will take uncommitted blob as a non-exist blob and throw exception.
* @returns {Promise}
* @memberof LokiBlobMetadataStore
*/
- private async getBlobWithLeaseUpdated(
- account: string,
- container: string,
- blob: string,
- snapshot: string | undefined,
- context: Context
- ): Promise;
private async getBlobWithLeaseUpdated(
account: string,
container: string,
blob: string,
snapshot: string | undefined,
context: Context,
- // tslint:disable-next-line:unified-signatures
- forceExist: true
+ forceExist?: true,
+ forceCommitted?: boolean
): Promise;
+
+ /**
+ * Get a blob document model from Loki collection.
+ * Will NOT throw BlobNotFound storage error if blob doesn't exist.
+ *
+ * @private
+ * @param {string} account
+ * @param {string} container
+ * @param {string} blob
+ * @param {(string | undefined)} snapshot
+ * @param {Context} context
+ * @param {false} forceExist
+ * @param {boolean} [forceCommitted] If true, will take uncommitted blob as a non-exist blob and return undefined.
+ * @returns {(Promise)}
+ * @memberof LokiBlobMetadataStore
+ */
private async getBlobWithLeaseUpdated(
account: string,
container: string,
blob: string,
snapshot: string | undefined,
context: Context,
- forceExist: false
+ forceExist: false,
+ forceCommitted?: boolean
): Promise;
+
private async getBlobWithLeaseUpdated(
account: string,
container: string,
blob: string,
snapshot: string = "",
context: Context,
- forceExist?: boolean
+ forceExist?: boolean,
+ forceCommitted?: boolean
): Promise {
await this.checkContainerExist(context, account, container);
@@ -2546,14 +2963,27 @@ export default class LokiBlobMetadataStore
snapshot
});
+ // Force exist if parameter forceExist is undefined or true
if (forceExist === undefined || forceExist === true) {
- if (!doc) {
- throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ if (forceCommitted) {
+ if (!doc || !(doc as BlobModel).isCommitted) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+ } else {
+ if (!doc) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+ }
+ } else {
+ if (forceCommitted) {
+ if (!doc || !(doc as BlobModel).isCommitted) {
+ return undefined;
+ }
+ } else {
+ if (!doc) {
+ return undefined;
+ }
}
- }
-
- if (!doc) {
- return undefined;
}
if (doc.properties) {
@@ -2571,7 +3001,7 @@ export default class LokiBlobMetadataStore
leaseBreakTime: undefined,
leaseDurationType: undefined,
leaseState: Models.LeaseStateType.Available, // TODO: Lease state & status should be undefined for snapshots
- leaseStatus: LeaseStatusType.Unlocked // TODO: Lease state & status should be undefined for snapshots
+ leaseStatus: Models.LeaseStatusType.Unlocked // TODO: Lease state & status should be undefined for snapshots
});
} else {
LeaseFactory.createLeaseState(new BlobLeaseAdapter(doc), context).sync(
diff --git a/src/blob/persistence/SqlBlobMetadataStore.ts b/src/blob/persistence/SqlBlobMetadataStore.ts
index 3e8e72ca9..aa74eca72 100644
--- a/src/blob/persistence/SqlBlobMetadataStore.ts
+++ b/src/blob/persistence/SqlBlobMetadataStore.ts
@@ -15,9 +15,10 @@ import {
DEFAULT_SQL_COLLATE
} from "../../common/utils/constants";
import { convertDateTimeStringMsTo7Digital } from "../../common/utils/utils";
+import { validateReadConditions } from "../conditions/ReadConditionalHeadersValidator";
+import { validateWriteConditions } from "../conditions/WriteConditionalHeadersValidator";
import StorageErrorFactory from "../errors/StorageErrorFactory";
import * as Models from "../generated/artifacts/models";
-import { BlobType, LeaseAccessConditions } from "../generated/artifacts/models";
import Context from "../generated/Context";
import BlobLeaseAdapter from "../lease/BlobLeaseAdapter";
import BlobLeaseSyncer from "../lease/BlobLeaseSyncer";
@@ -591,12 +592,18 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
context: Context,
account: string,
container: string,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ options: Models.ContainerDeleteMethodOptionalParams = {}
): Promise {
await this.sequelize.transaction(async t => {
/* Transaction starts */
const findResult = await ContainersModel.findOne({
- attributes: ["lease"],
+ attributes: [
+ "accountName",
+ "containerName",
+ "etag",
+ "lastModified",
+ "lease"
+ ],
where: {
accountName: account,
containerName: container
@@ -604,6 +611,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -611,7 +624,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
LeaseFactory.createLeaseState(
this.convertDbModelToLease(findResult),
context
- ).validate(new ContainerDeleteLeaseValidator(leaseAccessConditions));
+ ).validate(
+ new ContainerDeleteLeaseValidator(options.leaseAccessConditions)
+ );
await ContainersModel.destroy({
where: {
@@ -659,12 +674,19 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
lastModified: Date,
etag: string,
metadata?: IContainerMetadata,
- leaseAccessConditions?: LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
return this.sequelize.transaction(async t => {
/* Transaction starts */
const findResult = await ContainersModel.findOne({
- attributes: ["lease"],
+ attributes: [
+ "accountName",
+ "containerName",
+ "etag",
+ "lastModified",
+ "lease"
+ ],
where: {
accountName: account,
containerName: container
@@ -672,6 +694,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -739,7 +767,13 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
): Promise {
await this.sequelize.transaction(async t => {
const findResult = await ContainersModel.findOne({
- attributes: ["lease"],
+ attributes: [
+ "accountName",
+ "containerName",
+ "etag",
+ "lastModified",
+ "lease"
+ ],
where: {
accountName: account,
containerName: container
@@ -747,6 +781,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ setAclModel.modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -796,6 +836,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -831,7 +877,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
context: Context,
account: string,
container: string,
- leaseId: string
+ leaseId: string,
+ options: Models.ContainerReleaseLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
/* Transaction starts */
@@ -843,6 +890,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -876,7 +929,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
context: Context,
account: string,
container: string,
- leaseId: string
+ leaseId: string,
+ options: Models.ContainerRenewLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
/* Transaction starts */
@@ -889,6 +943,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -925,7 +985,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
context: Context,
account: string,
container: string,
- breakPeriod: number | undefined
+ breakPeriod: number | undefined,
+ options: Models.ContainerBreakLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
const findResult = await ContainersModel.findOne({
@@ -936,6 +997,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -982,7 +1049,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
account: string,
container: string,
leaseId: string,
- proposedLeaseId: string
+ proposedLeaseId: string,
+ options: Models.ContainerChangeLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
const findResult = await ContainersModel.findOne({
@@ -993,6 +1061,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ findResult ? this.convertDbModelToContainerModel(findResult) : undefined
+ );
+
if (findResult === null || findResult === undefined) {
throw StorageErrorFactory.getContainerNotFound(context.contextId);
}
@@ -1043,7 +1117,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
public async createBlob(
context: Context,
blob: BlobModel,
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -1070,6 +1145,23 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult)
+ : undefined
+ );
+
+ // Create if not exists
+ if (
+ modifiedAccessConditions &&
+ modifiedAccessConditions.ifNoneMatch === "*" &&
+ blobFindResult
+ ) {
+ throw StorageErrorFactory.getBlobAlreadyExists(context.contextId);
+ }
+
if (blobFindResult) {
const blobModel: BlobModel = this.convertDbModelToBlobModel(
blobFindResult
@@ -1100,7 +1192,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
container: string,
blob: string,
snapshot: string = "",
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -1127,6 +1220,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateReadConditions(
+ context,
+ modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult)
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -1286,8 +1387,7 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
containerName: block.containerName,
blobName: block.blobName,
snapshot: "",
- deleting: 0,
- isCommitted: true
+ deleting: 0
},
transaction: t
});
@@ -1297,10 +1397,34 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
blobFindResult
);
- LeaseFactory.createLeaseState(
- new BlobLeaseAdapter(blobModel),
- context
- ).validate(new BlobWriteLeaseValidator(leaseAccessConditions));
+ if (blobModel.isCommitted === true) {
+ LeaseFactory.createLeaseState(
+ new BlobLeaseAdapter(blobModel),
+ context
+ ).validate(new BlobWriteLeaseValidator(leaseAccessConditions));
+ }
+
+ // If the new block ID does not have same length with before uncommited block ID, return failure.
+ const existBlock = await BlocksModel.findOne({
+ attributes: ["blockName"],
+ where: {
+ accountName: block.accountName,
+ containerName: block.containerName,
+ blobName: block.blobName,
+ deleting: 0
+ },
+ order: [["id", "ASC"]],
+ transaction: t
+ });
+ if (
+ existBlock &&
+ Buffer.from(
+ this.getModelValue(existBlock, "blockName", true),
+ "base64"
+ ).length !== Buffer.from(block.name, "base64").length
+ ) {
+ throw StorageErrorFactory.getInvalidBlobOrBlock(context.contextId);
+ }
} else {
const newBlob = {
deleted: false,
@@ -1407,7 +1531,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
context: Context,
blob: BlobModel,
blockList: { blockName: string; blockCommitType: string }[],
- leaseAccessConditions: Models.LeaseAccessConditions | undefined
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
await this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -1444,6 +1569,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce duplicated convert
+ : undefined
+ );
+
let creationTime = blob.properties.creationTime || context.startTime;
if (blobFindResult !== null && blobFindResult !== undefined) {
@@ -1451,6 +1584,16 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
blobFindResult
);
+ // Create if not exists
+ if (
+ modifiedAccessConditions &&
+ modifiedAccessConditions.ifNoneMatch === "*" &&
+ blobModel &&
+ blobModel.isCommitted
+ ) {
+ throw StorageErrorFactory.getBlobAlreadyExists(context.contextId);
+ }
+
creationTime = blobModel.properties.creationTime || creationTime;
LeaseFactory.createLeaseState(
@@ -1530,7 +1673,7 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
.reduce((total, val) => {
return total + val;
}, 0),
- blobType: BlobType.BlockBlob
+ blobType: Models.BlobType.BlockBlob
}
};
@@ -1570,7 +1713,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
container: string,
blob: string,
snapshot: string = "",
- leaseAccessConditions?: Models.LeaseAccessConditions
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -1597,6 +1741,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateReadConditions(
+ context,
+ modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult)
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -1605,6 +1757,10 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
blobFindResult
);
+ if (!blobModel.isCommitted) {
+ throw StorageErrorFactory.getBlobNotFound(context.contextId);
+ }
+
return LeaseFactory.createLeaseState(
new BlobLeaseAdapter(blobModel),
context
@@ -1624,7 +1780,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
container: string,
blob: string,
leaseAccessConditions?: Models.LeaseAccessConditions,
- metadata?: Models.BlobMetadata
+ metadata?: Models.BlobMetadata,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -1652,6 +1809,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateReadConditions(
+ context,
+ modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -1719,11 +1884,19 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
blobName: blob,
snapshot: options.snapshot === undefined ? "" : options.snapshot,
deleting: 0,
- isCommitted: true
+ isCommitted: true // TODO: Support deleting uncommitted block blob
},
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -1873,7 +2046,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
container: string,
blob: string,
leaseAccessConditions: Models.LeaseAccessConditions | undefined,
- blobHTTPHeaders: Models.BlobHTTPHeaders | undefined
+ blobHTTPHeaders: Models.BlobHTTPHeaders | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -1901,6 +2075,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -1923,12 +2105,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
blobHTTPHeaders.blobContentLanguage;
blobModel.properties.contentDisposition =
blobHTTPHeaders.blobContentDisposition;
- blobModel.properties.lastModified = context.startTime
- ? context.startTime
- : new Date();
}
blobModel.properties.etag = newEtag();
+ blobModel.properties.lastModified = context.startTime
+ ? context.startTime
+ : new Date();
await BlobsModel.update(this.convertBlobModelToDbModel(blobModel), {
where: {
@@ -1951,7 +2133,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
container: string,
blob: string,
leaseAccessConditions: Models.LeaseAccessConditions | undefined,
- metadata: Models.BlobMetadata | undefined
+ metadata: Models.BlobMetadata | undefined,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -1979,6 +2162,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -2029,7 +2220,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
container: string,
blob: string,
duration: number,
- proposedLeaseId?: string
+ proposedLeaseId?: string,
+ options: Models.BlobAcquireLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -2057,6 +2249,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -2087,7 +2287,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
account: string,
container: string,
blob: string,
- leaseId: string
+ leaseId: string,
+ options: Models.BlobReleaseLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -2115,6 +2316,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -2145,7 +2354,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
account: string,
container: string,
blob: string,
- leaseId: string
+ leaseId: string,
+ options: Models.BlobRenewLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -2173,6 +2383,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -2204,7 +2422,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
container: string,
blob: string,
leaseId: string,
- proposedLeaseId: string
+ proposedLeaseId: string,
+ options: Models.BlobChangeLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -2232,6 +2451,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -2262,7 +2489,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
account: string,
container: string,
blob: string,
- breakPeriod: number | undefined
+ breakPeriod: number | undefined,
+ options: Models.BlobBreakLeaseOptionalParams = {}
): Promise {
return this.sequelize.transaction(async t => {
const containerFindResult = await ContainersModel.findOne({
@@ -2290,6 +2518,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
transaction: t
});
+ validateWriteConditions(
+ context,
+ options.modifiedAccessConditions,
+ blobFindResult
+ ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert
+ : undefined
+ );
+
if (blobFindResult === null || blobFindResult === undefined) {
throw StorageErrorFactory.getBlobNotFound(context.contextId);
}
@@ -2515,7 +2751,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
blob: BlobModel,
start: number,
end: number,
- persistencycontext: import("./IBlobMetadataStore").IExtentChunk
+ persistency: IExtentChunk,
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
throw new Error("Method not implemented.");
}
@@ -2524,7 +2762,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
context: Context,
blob: BlobModel,
start: number,
- end: number
+ end: number,
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
throw new Error("Method not implemented.");
}
@@ -2534,7 +2774,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
account: string,
container: string,
blob: string,
- snapshot?: string | undefined
+ snapshot?: string | undefined,
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
throw new Error("Method not implemented.");
}
@@ -2544,7 +2786,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore {
account: string,
container: string,
blob: string,
- blobContentLength: number
+ blobContentLength: number,
+ leaseAccessConditions?: Models.LeaseAccessConditions,
+ modifiedAccessConditions?: Models.ModifiedAccessConditions
): Promise {
throw new Error("Method not implemented.");
}
diff --git a/src/blob/utils/constants.ts b/src/blob/utils/constants.ts
index 910c8c96e..d6e21206f 100644
--- a/src/blob/utils/constants.ts
+++ b/src/blob/utils/constants.ts
@@ -1,7 +1,7 @@
import { StoreDestinationArray } from "../../common/persistence/IExtentStore";
import * as Models from "../generated/artifacts/models";
-export const VERSION = "3.5.0";
+export const VERSION = "3.6.0";
export const BLOB_API_VERSION = "2019-07-07";
export const DEFAULT_BLOB_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access
export const DEFAULT_LIST_BLOBS_MAX_RESULTS = 5000;
@@ -52,6 +52,8 @@ export const HeaderConstants = {
X_MS_IF_SEQUENCE_NUMBER_EQ: "x-ms-if-sequence-number-eq",
X_MS_BLOB_CONDITION_MAXSIZE: "x-ms-blob-condition-maxsize",
X_MS_BLOB_CONDITION_APPENDPOS: "x-ms-blob-condition-appendpos",
+ X_MS_SEQUENCE_NUMBER_ACTION: "x-ms-sequence-number-action",
+ X_MS_BLOB_SEQUENCE_NUMBER: "x-ms-blob-sequence-number",
X_MS_CONTENT_CRC64: "x-ms-content-crc64",
X_MS_RANGE_GET_CONTENT_CRC64: "x-ms-range-get-content-crc64",
X_MS_ENCRYPTION_KEY: "x-ms-encryption-key",
diff --git a/src/common/utils/constants.ts b/src/common/utils/constants.ts
index e2f3cb1d9..153558f33 100644
--- a/src/common/utils/constants.ts
+++ b/src/common/utils/constants.ts
@@ -24,3 +24,9 @@ export const DEFAULT_SQL_OPTIONS = {
timezone: "+00:00"
}
};
+
+// In some scenarios, users want to test with production-style URLs like
+// http[s]://devstoreaccount1.localhost[:port]/container/path/blob.dat
+// (as opposed to default emulator style http[s]://hostname[:port]/devstoreaccount1/container/path/blob.dat
+// When URL's hostname ends with .localhost, we assume user wants to use production-style URL format.
+export const PRODUCTION_STYLE_URL_HOSTNAME = ".localhost";
diff --git a/src/common/utils/utils.ts b/src/common/utils/utils.ts
index 757b9e93f..81afd470f 100644
--- a/src/common/utils/utils.ts
+++ b/src/common/utils/utils.ts
@@ -31,3 +31,30 @@ export function convertDateTimeStringMsTo7Digital(
): string {
return dateTimeString.replace("Z", "0000Z");
}
+
+export function convertRawHeadersToMetadata(
+ rawHeaders: string[] = []
+): { [propertyName: string]: string } | undefined {
+ const metadataPrefix = "x-ms-meta-";
+ const res: { [propertyName: string]: string } = {};
+ let isEmpty = true;
+
+ for (let i = 0; i < rawHeaders.length; i = i + 2) {
+ const header = rawHeaders[i];
+ if (
+ header.startsWith(metadataPrefix) &&
+ header.length > metadataPrefix.length
+ ) {
+ const key = header.substr(metadataPrefix.length);
+ let value = rawHeaders[i + 1] || "";
+ if (res[key] !== undefined) {
+ value = `${res[key]},${value}`;
+ }
+ res[key] = value;
+ isEmpty = false;
+ continue;
+ }
+ }
+
+ return isEmpty ? undefined : res;
+}
diff --git a/src/queue/QueueRequestListenerFactory.ts b/src/queue/QueueRequestListenerFactory.ts
index 4ee386ed2..377bd5c55 100644
--- a/src/queue/QueueRequestListenerFactory.ts
+++ b/src/queue/QueueRequestListenerFactory.ts
@@ -21,6 +21,7 @@ import PreflightMiddlewareFactory from "./middlewares/PreflightMiddlewareFactory
import queueStorageContextMiddleware from "./middlewares/queueStorageContext.middleware";
import { IQueueMetadataStore } from "./persistence/IQueueMetadataStore";
import { DEFAULT_QUEUE_CONTEXT_PATH } from "./utils/constants";
+import QueueTokenAuthenticator from "./authentication/QueueTokenAuthenticator";
/**
* Default RequestListenerFactory based on express framework.
@@ -103,7 +104,8 @@ export default class QueueRequestListenerFactory
this.accountDataStore,
this.metadataStore,
logger
- )
+ ),
+ new QueueTokenAuthenticator(this.accountDataStore, logger)
])
);
diff --git a/src/queue/authentication/QueueTokenAuthenticator.ts b/src/queue/authentication/QueueTokenAuthenticator.ts
new file mode 100644
index 000000000..a9a45dded
--- /dev/null
+++ b/src/queue/authentication/QueueTokenAuthenticator.ts
@@ -0,0 +1,68 @@
+import IAccountDataStore from "../../common/IAccountDataStore";
+import ILogger from "../../common/ILogger";
+import QueueStorageContext from "../context/QueueStorageContext";
+import StorageErrorFactory from "../errors/StorageErrorFactory";
+import Context from "../generated/Context";
+import IRequest from "../generated/IRequest";
+import { HeaderConstants } from "../utils/constants";
+import IAuthenticator from "./IAuthenticator";
+
+export default class QueueTokenAuthenticator implements IAuthenticator {
+ public constructor(
+ private readonly dataStore: IAccountDataStore,
+ private readonly logger: ILogger
+ ) {}
+
+ public async validate(
+ req: IRequest,
+ context: Context
+ ): Promise {
+ const queueContext = new QueueStorageContext(context);
+ const account = queueContext.account!;
+
+ this.logger.info(
+ `QueueTokenAuthenticator:validate() Start validation against token authentication.`,
+ queueContext.contextID
+ );
+
+ // TODO: Make following async
+ const accountProperties = this.dataStore.getAccount(account);
+ if (accountProperties === undefined) {
+ this.logger.error(
+ `QueueTokenAuthenticator:validate() Invalid storage account ${account}.`,
+ queueContext.contextID
+ );
+ throw StorageErrorFactory.getInvalidOperation(
+ queueContext.contextID!,
+ "Invalid storage account."
+ );
+ }
+
+ const authHeaderValue = req.getHeader(HeaderConstants.AUTHORIZATION);
+ if (authHeaderValue === undefined) {
+ this.logger.info(
+ // tslint:disable-next-line:max-line-length
+ `QueueTokenAuthenticator:validate() Request doesn't include valid authentication header. Skip token authentication.`,
+ queueContext.contextID
+ );
+ return undefined;
+ } else {
+ const hasBearerToken = authHeaderValue.startsWith("Bearer");
+
+ if (hasBearerToken) {
+ this.logger.info(
+ // tslint:disable-next-line:max-line-length
+ `QueueTokenAuthenticator:validate() Request includes Bearer token.`,
+ queueContext.contextID
+ );
+ } else {
+ this.logger.info(
+ // tslint:disable-next-line:max-line-length
+ `QueueTokenAuthenticator:validate() Request does not include Bearer token. Skip token authentication.`,
+ queueContext.contextID
+ );
+ }
+ return hasBearerToken;
+ }
+ }
+}
diff --git a/src/queue/errors/StorageError.ts b/src/queue/errors/StorageError.ts
index 40cd94030..222469971 100644
--- a/src/queue/errors/StorageError.ts
+++ b/src/queue/errors/StorageError.ts
@@ -10,6 +10,10 @@ import { jsonToXML } from "../generated/utils/xml";
* @extends {MiddlewareError}
*/
export default class StorageError extends MiddlewareError {
+ public readonly storageErrorCode: string;
+ public readonly storageErrorMessage: string;
+ public readonly storageRequestID: string;
+
/**
* Creates an instance of StorageError.
*
@@ -56,5 +60,8 @@ export default class StorageError extends MiddlewareError {
);
this.name = "StorageError";
+ this.storageErrorCode = storageErrorCode;
+ this.storageErrorMessage = storageErrorMessage;
+ this.storageRequestID = storageRequestID;
}
}
diff --git a/src/queue/generated/ExpressRequestAdapter.ts b/src/queue/generated/ExpressRequestAdapter.ts
index dced6a1ec..c37abcb31 100644
--- a/src/queue/generated/ExpressRequestAdapter.ts
+++ b/src/queue/generated/ExpressRequestAdapter.ts
@@ -43,6 +43,10 @@ export default class ExpressRequestAdapter implements IRequest {
return this.req.headers;
}
+ public getRawHeaders(): string[] {
+ return this.req.rawHeaders;
+ }
+
public getQuery(key: string): string | undefined {
return this.req.query[key];
}
diff --git a/src/queue/generated/IRequest.ts b/src/queue/generated/IRequest.ts
index 0fb3060ea..df39dd0ad 100644
--- a/src/queue/generated/IRequest.ts
+++ b/src/queue/generated/IRequest.ts
@@ -19,6 +19,7 @@ export default interface IRequest {
getBody(): string | undefined;
getHeader(field: string): string | undefined;
getHeaders(): { [header: string]: string | string[] | undefined };
+ getRawHeaders(): string[];
getQuery(key: string): string | undefined;
getProtocol(): string;
}
diff --git a/src/queue/middlewares/queueStorageContext.middleware.ts b/src/queue/middlewares/queueStorageContext.middleware.ts
index 501d44120..bca2b7f95 100644
--- a/src/queue/middlewares/queueStorageContext.middleware.ts
+++ b/src/queue/middlewares/queueStorageContext.middleware.ts
@@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from "express";
import uuid from "uuid/v4";
import logger from "../../common/Logger";
+import { PRODUCTION_STYLE_URL_HOSTNAME } from "../../common/utils/constants";
import { checkApiVersion } from "../../common/utils/utils";
import QueueStorageContext from "../context/QueueStorageContext";
import StorageErrorFactory from "../errors/StorageErrorFactory";
@@ -62,7 +63,7 @@ export default function queueStorageContextMiddleware(
message,
messageId,
isSecondary
- ] = extractStoragePartsFromPath(req.path);
+ ] = extractStoragePartsFromPath(req.hostname, req.path);
queueContext.account = account;
queueContext.queue = queue;
@@ -70,7 +71,8 @@ export default function queueStorageContextMiddleware(
queueContext.messageId = messageId;
queueContext.isSecondary = isSecondary;
- // Emulator's URL pattern is like http://hostname:port/account/queue/messages
+ // Emulator's URL pattern is like http://hostname[:port]/account/queue/messages
+ // (or, alternatively, http[s]://account.localhost[:port]/queue/messages)
// Create a router to exclude account name from req.path, as url path in swagger doesn't include account
// Exclude account name from req.path for dispatchMiddleware
queueContext.dispatchPattern =
@@ -149,6 +151,7 @@ export default function queueStorageContextMiddleware(
* @returns {([string | undefined, string | undefined, string | undefined, boolean | undefined])}
*/
export function extractStoragePartsFromPath(
+ hostname: string,
path: string
): [
string | undefined,
@@ -170,11 +173,19 @@ export function extractStoragePartsFromPath(
const parts = normalizedPath.split("/");
- account = parts[0];
- queue = parts[1];
+ let urlPartIndex = 0;
+ if (hostname.endsWith(PRODUCTION_STYLE_URL_HOSTNAME)) {
+ account = hostname.substring(
+ 0,
+ hostname.length - PRODUCTION_STYLE_URL_HOSTNAME.length
+ );
+ } else {
+ account = parts[urlPartIndex++];
+ }
+ queue = parts[urlPartIndex++];
// For delete and update, it is messages/messageid?popreceipt=string-value
- message = parts[2];
- messageId = parts[3];
+ message = parts[urlPartIndex++];
+ messageId = parts[urlPartIndex++];
if (account.endsWith(SECONDARY_SUFFIX)) {
account = account.substr(0, account.length - SECONDARY_SUFFIX.length);
diff --git a/src/queue/utils/constants.ts b/src/queue/utils/constants.ts
index 2c96236e8..24501f5ad 100644
--- a/src/queue/utils/constants.ts
+++ b/src/queue/utils/constants.ts
@@ -1,6 +1,6 @@
import { StoreDestinationArray } from "../../common/persistence/IExtentStore";
-export const VERSION = "3.5.0";
+export const VERSION = "3.6.0";
export const QUEUE_API_VERSION = "2019-07-07";
export const DEFAULT_QUEUE_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access
export const DEFAULT_QUEUE_LISTENING_PORT = 10001;
diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts
index 0016b33a7..989ad0542 100644
--- a/tests/blob/apis/blob.test.ts
+++ b/tests/blob/apis/blob.test.ts
@@ -80,6 +80,127 @@ describe("BlobAPIs", () => {
);
});
+ it("download should work with conditional headers @loki @sql", async () => {
+ const properties = await blobURL.getProperties(Aborter.none);
+ const result = await blobURL.download(Aborter.none, 0, undefined, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifMatch: properties.eTag,
+ ifNoneMatch: "invalidetag",
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2188/01/01")
+ }
+ }
+ });
+ assert.deepStrictEqual(await bodyToString(result, content.length), content);
+ assert.equal(result.contentRange, undefined);
+ assert.equal(
+ result._response.request.headers.get("x-ms-client-request-id"),
+ result.clientRequestId
+ );
+ });
+
+ it("download should work with ifMatch value * @loki @sql", async () => {
+ const result = await blobURL.download(Aborter.none, 0, undefined, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifMatch: "*,abc",
+ ifNoneMatch: "invalidetag",
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2188/01/01")
+ }
+ }
+ });
+ assert.deepStrictEqual(await bodyToString(result, content.length), content);
+ assert.equal(result.contentRange, undefined);
+ assert.equal(
+ result._response.request.headers.get("x-ms-client-request-id"),
+ result.clientRequestId
+ );
+ });
+
+ it("download should not work with invalid conditional header ifMatch @loki @sql", async () => {
+ const properties = await blobURL.getProperties(Aborter.none);
+ try {
+ await blobURL.download(Aborter.none, 0, undefined, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifMatch: properties.eTag + "invalid"
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("download should not work with conditional header ifNoneMatch @loki @sql", async () => {
+ const properties = await blobURL.getProperties(Aborter.none);
+ try {
+ await blobURL.download(Aborter.none, 0, undefined, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifNoneMatch: properties.eTag
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 304);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("download should not work with conditional header ifNoneMatch * @loki @sql", async () => {
+ try {
+ await blobURL.download(Aborter.none, 0, undefined, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifNoneMatch: "*"
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 400);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("download should not work with conditional header ifModifiedSince @loki @sql", async () => {
+ try {
+ await blobURL.download(Aborter.none, 0, undefined, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifModifiedSince: new Date("2120/01/01")
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 304);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("download should not work with conditional header ifUnmodifiedSince @loki @sql", async () => {
+ try {
+ await blobURL.download(Aborter.none, 0, undefined, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifUnmodifiedSince: new Date("2018/01/01")
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
it("download all parameters set @loki @sql", async () => {
const result = await blobURL.download(Aborter.none, 0, 1, {
rangeGetContentMD5: true
@@ -128,6 +249,146 @@ describe("BlobAPIs", () => {
);
});
+ it("delete should work for valid ifMatch @loki @sql", async () => {
+ const properties = await blobURL.getProperties(Aborter.none);
+
+ const result = await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifMatch: properties.eTag
+ }
+ }
+ });
+ assert.equal(
+ result._response.request.headers.get("x-ms-client-request-id"),
+ result.clientRequestId
+ );
+ });
+
+ it("delete should work for * ifMatch @loki @sql", async () => {
+ const result = await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifMatch: "*"
+ }
+ }
+ });
+ assert.equal(
+ result._response.request.headers.get("x-ms-client-request-id"),
+ result.clientRequestId
+ );
+ });
+
+ it("delete should not work for invalid ifMatch @loki @sql", async () => {
+ try {
+ await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifMatch: "invalid"
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("delete should work for valid ifNoneMatch @loki @sql", async () => {
+ const result = await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifNoneMatch: "unmatchetag"
+ }
+ }
+ });
+ assert.equal(
+ result._response.request.headers.get("x-ms-client-request-id"),
+ result.clientRequestId
+ );
+ });
+
+ it("delete should not work for invalid ifNoneMatch @loki @sql", async () => {
+ const properties = await blobURL.getProperties(Aborter.none);
+
+ try {
+ await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifNoneMatch: properties.eTag
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("delete should work for ifNoneMatch * @loki @sql", async () => {
+ await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifNoneMatch: "*"
+ }
+ }
+ });
+ });
+
+ it("delete should work for valid ifModifiedSince * @loki @sql", async () => {
+ await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifModifiedSince: new Date("2018/01/01")
+ }
+ }
+ });
+ });
+
+ it("delete should not work for invalid ifModifiedSince @loki @sql", async () => {
+ try {
+ await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifModifiedSince: new Date("2118/01/01")
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("delete should work for valid ifUnmodifiedSince * @loki @sql", async () => {
+ await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifUnmodifiedSince: new Date("2118/01/01")
+ }
+ }
+ });
+ });
+
+ it("delete should not work for invalid ifUnmodifiedSince @loki @sql", async () => {
+ try {
+ await blobURL.delete(Aborter.none, {
+ blobAccessConditions: {
+ modifiedAccessConditions: {
+ ifUnmodifiedSince: new Date("2018/01/01")
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
it("should create a snapshot from a blob @loki @sql", async () => {
const result = await blobURL.createSnapshot(Aborter.none);
assert.ok(result.snapshot);
diff --git a/tests/blob/apis/blockblob.test.ts b/tests/blob/apis/blockblob.test.ts
index 917cbd5c3..1cbe5ad6d 100644
--- a/tests/blob/apis/blockblob.test.ts
+++ b/tests/blob/apis/blockblob.test.ts
@@ -282,6 +282,44 @@ describe("BlockBlobAPIs", () => {
assert.deepStrictEqual(await bodyToString(result, 0), "");
});
+ it("commitBlockList with empty list should not work with ifNoneMatch=* for existing blob @loki @sql", async () => {
+ await blockBlobURL.commitBlockList(Aborter.none, []);
+
+ try {
+ await blockBlobURL.commitBlockList(Aborter.none, [], {
+ accessConditions: {
+ modifiedAccessConditions: {
+ ifNoneMatch: "*"
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 409);
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("upload should not work with ifNoneMatch=* for existing blob @loki @sql", async () => {
+ await blockBlobURL.commitBlockList(Aborter.none, []);
+
+ try {
+ await blockBlobURL.upload(Aborter.none, "hello", 5, {
+ accessConditions: {
+ modifiedAccessConditions: {
+ ifNoneMatch: "*"
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 409);
+ return;
+ }
+
+ assert.fail();
+ });
+
it("commitBlockList with all parameters set @loki @sql", async () => {
const body = "HelloWorld";
await blockBlobURL.stageBlock(
@@ -383,6 +421,29 @@ describe("BlockBlobAPIs", () => {
body,
body.length
);
+
+ // Getproperties on a block blob without commited block will return 404
+ let err;
+ try {
+ await blockBlobURL.getProperties(Aborter.none);
+ } catch (error) {
+ err = error;
+ }
+ assert.deepStrictEqual(err.statusCode, 404);
+
+ // Stage block with block Id length different than the exist uncommited blocks will fail with 400
+ try {
+ await blockBlobURL.stageBlock(
+ Aborter.none,
+ base64encode("123"),
+ body,
+ body.length
+ );
+ } catch (error) {
+ err = error;
+ }
+ assert.deepStrictEqual(err.statusCode, 400);
+
await blockBlobURL.commitBlockList(Aborter.none, [
base64encode("1"),
base64encode("2")
@@ -390,7 +451,7 @@ describe("BlockBlobAPIs", () => {
await blockBlobURL.stageBlock(
Aborter.none,
- base64encode("3"),
+ base64encode("123"),
body,
body.length
);
@@ -408,7 +469,7 @@ describe("BlockBlobAPIs", () => {
listResponse = await blockBlobURL.getBlockList(Aborter.none, "uncommitted");
assert.equal(listResponse.uncommittedBlocks!.length, 1);
- assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("3"));
+ assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("123"));
assert.equal(listResponse.uncommittedBlocks![0].size, body.length);
assert.equal(listResponse.committedBlocks!.length, 0);
@@ -419,7 +480,7 @@ describe("BlockBlobAPIs", () => {
assert.equal(listResponse.committedBlocks![1].name, base64encode("2"));
assert.equal(listResponse.committedBlocks![1].size, body.length);
assert.equal(listResponse.uncommittedBlocks!.length, 1);
- assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("3"));
+ assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("123"));
assert.equal(listResponse.uncommittedBlocks![0].size, body.length);
});
diff --git a/tests/blob/apis/container.test.ts b/tests/blob/apis/container.test.ts
index 479d400d4..c4f8331d8 100644
--- a/tests/blob/apis/container.test.ts
+++ b/tests/blob/apis/container.test.ts
@@ -70,6 +70,42 @@ describe("ContainerAPIs", () => {
assert.deepEqual(result.metadata, metadata);
});
+ it("setMetadata should work with conditional headers @loki @sql", async () => {
+ // const properties = await containerURL.getProperties(Aborter.none);
+ await containerURL.setMetadata(
+ Aborter.none,
+ {},
+ {
+ containerAccessConditions: {
+ modifiedAccessConditions: {
+ ifModifiedSince: new Date("2018/01/01")
+ }
+ }
+ }
+ );
+ });
+
+ it("setMetadata should not work with invalid conditional headers @loki @sql", async () => {
+ try {
+ await containerURL.setMetadata(
+ Aborter.none,
+ {},
+ {
+ containerAccessConditions: {
+ modifiedAccessConditions: {
+ ifModifiedSince: new Date("2118/01/01")
+ }
+ }
+ }
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+
+ assert.fail();
+ });
+
it("getProperties @loki @sql", async () => {
const result = await containerURL.getProperties(Aborter.none);
assert.ok(result.eTag!.length > 0);
diff --git a/tests/blob/apis/pageblob.test.ts b/tests/blob/apis/pageblob.test.ts
index acf099183..d91a1aba6 100644
--- a/tests/blob/apis/pageblob.test.ts
+++ b/tests/blob/apis/pageblob.test.ts
@@ -9,6 +9,7 @@ import {
} from "@azure/storage-blob";
import assert = require("assert");
+import { SequenceNumberActionType } from "../../../src/blob/generated/artifacts/models";
import { configLogger } from "../../../src/common/Logger";
import BlobTestServerFactory from "../../BlobTestServerFactory";
import {
@@ -133,6 +134,7 @@ describe("PageBlobAPIs", () => {
properties.contentType,
options.blobHTTPHeaders.blobContentType
);
+ assert.equal(0, properties.blobSequenceNumber);
assert.equal(properties.metadata!.key1, options.metadata.key1);
assert.equal(properties.metadata!.key2, options.metadata.key2);
assert.equal(
@@ -290,6 +292,141 @@ describe("PageBlobAPIs", () => {
assert.equal(await bodyToString(page2, 512), "b".repeat(512));
});
+ it("uploadPages should work with sequence number conditions @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+
+ await pageBlobURL.updateSequenceNumber(
+ Aborter.none,
+ SequenceNumberActionType.Update,
+ 10
+ );
+
+ const result = await blobURL.download(Aborter.none, 0);
+ assert.equal(await bodyToString(result, 1024), "\u0000".repeat(1024));
+
+ await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberEqualTo: 10,
+ ifSequenceNumberLessThan: 11,
+ ifSequenceNumberLessThanOrEqualTo: 10
+ }
+ }
+ });
+ const result_upload = await pageBlobURL.uploadPages(
+ Aborter.none,
+ "b".repeat(512),
+ 512,
+ 512
+ );
+ assert.equal(
+ result_upload._response.request.headers.get("x-ms-client-request-id"),
+ result_upload.clientRequestId
+ );
+
+ const page1 = await pageBlobURL.download(Aborter.none, 0, 512);
+ const page2 = await pageBlobURL.download(Aborter.none, 512, 512);
+
+ assert.equal(await bodyToString(page1, 512), "a".repeat(512));
+ assert.equal(await bodyToString(page2, 512), "b".repeat(512));
+ });
+
+ it("uploadPages should not work if ifSequenceNumberEqualTo doesn't match @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+
+ await pageBlobURL.updateSequenceNumber(
+ Aborter.none,
+ SequenceNumberActionType.Update,
+ 10
+ );
+
+ try {
+ await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberEqualTo: 11
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("uploadPages should not work if ifSequenceNumberLessThan doesn't match @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+
+ await pageBlobURL.updateSequenceNumber(
+ Aborter.none,
+ SequenceNumberActionType.Update,
+ 10
+ );
+
+ try {
+ await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThan: 10
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+
+ try {
+ await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThan: 9
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("uploadPages should not work if ifSequenceNumberLessThanOrEqualTo doesn't match @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+
+ await pageBlobURL.updateSequenceNumber(
+ Aborter.none,
+ SequenceNumberActionType.Update,
+ 10
+ );
+
+ await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThanOrEqualTo: 10
+ }
+ }
+ });
+
+ try {
+ await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThanOrEqualTo: 9
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+
+ assert.fail();
+ });
+
it("uploadPages with sequential pages @loki", async () => {
const length = 512 * 3;
await pageBlobURL.create(Aborter.none, length);
@@ -1117,6 +1254,96 @@ describe("PageBlobAPIs", () => {
);
});
+ it("clearPages should work with sequence number conditions @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+ await pageBlobURL.clearPages(Aborter.none, 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberEqualTo: 0,
+ ifSequenceNumberLessThan: 1,
+ ifSequenceNumberLessThanOrEqualTo: 0
+ }
+ }
+ });
+ });
+
+ it("clearPages should not work with invalid ifSequenceNumberEqualTo @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+ try {
+ await pageBlobURL.clearPages(Aborter.none, 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberEqualTo: 1
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("clearPages should not work with invalid ifSequenceNumberLessThan @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+ await pageBlobURL.updateSequenceNumber(
+ Aborter.none,
+ SequenceNumberActionType.Increment
+ );
+
+ await pageBlobURL.clearPages(Aborter.none, 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThan: 2
+ }
+ }
+ });
+
+ try {
+ await pageBlobURL.clearPages(Aborter.none, 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThan: 1
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
+ it("clearPages should not work with invalid ifSequenceNumberLessThanOrEqualTo @loki", async () => {
+ await pageBlobURL.create(Aborter.none, 1024);
+ await pageBlobURL.updateSequenceNumber(
+ Aborter.none,
+ SequenceNumberActionType.Increment
+ );
+
+ await pageBlobURL.clearPages(Aborter.none, 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThanOrEqualTo: 1
+ }
+ }
+ });
+
+ try {
+ await pageBlobURL.clearPages(Aborter.none, 0, 512, {
+ accessConditions: {
+ sequenceNumberAccessConditions: {
+ ifSequenceNumberLessThanOrEqualTo: 0
+ }
+ }
+ });
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, 412);
+ return;
+ }
+ assert.fail();
+ });
+
it("clearPages to internally override a sequential range @loki", async () => {
const length = 512 * 5;
await pageBlobURL.create(Aborter.none, length);
diff --git a/tests/blob/blockblob.highlevel.test.ts b/tests/blob/blockblob.highlevel.test.ts
index d5fb81607..e5709d338 100644
--- a/tests/blob/blockblob.highlevel.test.ts
+++ b/tests/blob/blockblob.highlevel.test.ts
@@ -169,6 +169,7 @@ describe("BlockBlobHighlevel", () => {
assert.ok(downloadedData.equals(uploadedData));
});
+ // tslint:disable-next-line: max-line-length
it("uploadFileToBlockBlob should update progress when blob >= BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES @loki @sql", async () => {
let eventTriggered = false;
const aborter = Aborter.none;
@@ -187,6 +188,7 @@ describe("BlockBlobHighlevel", () => {
assert.ok(eventTriggered);
});
+ // tslint:disable-next-line: max-line-length
it("uploadFileToBlockBlob should update progress when blob < BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES @loki @sql", async () => {
let eventTriggered = false;
const aborter = Aborter.none;
@@ -403,6 +405,7 @@ describe("BlockBlobHighlevel", () => {
assert.ok(downloadedData.equals(uploadedData));
});
+ // tslint:disable-next-line: max-line-length
it("bloburl.download should download full data successfully when internal stream unexpected ends @loki @sql", async () => {
await uploadFileToBlockBlob(Aborter.none, tempFileSmall, blockBlobURL, {
blockSize: 4 * 1024 * 1024,
diff --git a/tests/blob/conditions.test.ts b/tests/blob/conditions.test.ts
new file mode 100644
index 000000000..a155b4217
--- /dev/null
+++ b/tests/blob/conditions.test.ts
@@ -0,0 +1,1008 @@
+import assert = require("assert");
+
+import ConditionalHeadersAdapter from "../../src/blob/conditions/ConditionalHeadersAdapter";
+import ConditionResourceAdapter from "../../src/blob/conditions/ConditionResourceAdapter";
+import ReadConditionalHeadersValidator from "../../src/blob/conditions/ReadConditionalHeadersValidator";
+import WriteConditionalHeadersValidator from "../../src/blob/conditions/WriteConditionalHeadersValidator";
+import StorageErrorFactory from "../../src/blob/errors/StorageErrorFactory";
+import Context from "../../src/blob/generated/Context";
+import {
+ BlobModel,
+ ContainerModel
+} from "../../src/blob/persistence/IBlobMetadataStore";
+
+const context = { contextId: "" } as Context;
+
+describe("ConditionalHeadersAdapter", () => {
+ it("Should work with undefined values @loki @sql", () => {
+ const validator = new ConditionalHeadersAdapter(context);
+ assert.deepStrictEqual(validator.ifModifiedSince, undefined);
+ assert.deepStrictEqual(validator.ifUnmodifiedSince, undefined);
+ assert.deepStrictEqual(validator.ifMatch, undefined);
+ assert.deepStrictEqual(validator.ifNoneMatch, undefined);
+ });
+
+ it("Should work with single etags @loki @sql", () => {
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date("2020/01/01"),
+ ifUnmodifiedSince: new Date("2020/02/01"),
+ ifMatch: "etag1",
+ ifNoneMatch: "etag2"
+ };
+
+ const validator = new ConditionalHeadersAdapter(
+ context,
+ modifiedAccessConditions
+ );
+ assert.deepStrictEqual(
+ validator.ifModifiedSince,
+ modifiedAccessConditions.ifModifiedSince
+ );
+ assert.deepStrictEqual(
+ validator.ifUnmodifiedSince,
+ modifiedAccessConditions.ifUnmodifiedSince
+ );
+ assert.deepStrictEqual(validator.ifMatch, [
+ modifiedAccessConditions.ifMatch
+ ]);
+ assert.deepStrictEqual(validator.ifNoneMatch, [
+ modifiedAccessConditions.ifNoneMatch
+ ]);
+ });
+
+ it("Should work with multi etags @loki @sql", () => {
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date("2020/01/01"),
+ ifUnmodifiedSince: new Date("2020/02/01"),
+ ifMatch: "etag1,etag2",
+ ifNoneMatch: "etag3,etag4,etag5"
+ };
+
+ const validator = new ConditionalHeadersAdapter(
+ context,
+ modifiedAccessConditions
+ );
+ assert.deepStrictEqual(
+ validator.ifModifiedSince,
+ modifiedAccessConditions.ifModifiedSince
+ );
+ assert.deepStrictEqual(
+ validator.ifUnmodifiedSince,
+ modifiedAccessConditions.ifUnmodifiedSince
+ );
+ assert.deepStrictEqual(validator.ifMatch, ["etag1", "etag2"]);
+ assert.deepStrictEqual(validator.ifNoneMatch, ["etag3", "etag4", "etag5"]);
+ });
+
+ it("Should work with etags with quotes @loki @sql", () => {
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date("2020/01/01"),
+ ifUnmodifiedSince: new Date("2020/02/01"),
+ ifMatch: '"etag1","etag2"',
+ ifNoneMatch: 'etag3,"etag4",etag5'
+ };
+
+ const validator = new ConditionalHeadersAdapter(
+ context,
+ modifiedAccessConditions
+ );
+ assert.deepStrictEqual(
+ validator.ifModifiedSince,
+ modifiedAccessConditions.ifModifiedSince
+ );
+ assert.deepStrictEqual(
+ validator.ifUnmodifiedSince,
+ modifiedAccessConditions.ifUnmodifiedSince
+ );
+ assert.deepStrictEqual(validator.ifMatch, ["etag1", "etag2"]);
+ assert.deepStrictEqual(validator.ifNoneMatch, ["etag3", "etag4", "etag5"]);
+ });
+});
+
+describe("ConditionResourceAdapter", () => {
+ it("Should work with undefined or null resource @loki @sql", () => {
+ const conditionResourceAdapterUndefined = new ConditionResourceAdapter(
+ undefined
+ );
+ assert.deepStrictEqual(conditionResourceAdapterUndefined.exist, false);
+
+ const conditionResourceAdapterNull = new ConditionResourceAdapter(null);
+ assert.deepStrictEqual(conditionResourceAdapterNull.exist, false);
+ });
+
+ it("Should work with blob model @loki @sql", () => {
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2018/01/01")
+ }
+ } as BlobModel;
+
+ const conditionResourceAdapter = new ConditionResourceAdapter(blobModel);
+ assert.deepStrictEqual(conditionResourceAdapter.exist, true);
+ assert.deepStrictEqual(conditionResourceAdapter.etag, "etag1");
+ assert.deepStrictEqual(
+ conditionResourceAdapter.lastModified,
+ blobModel.properties.lastModified
+ );
+ });
+
+ it("Should work with container model @loki @sql", () => {
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date()
+ }
+ } as ContainerModel;
+
+ const conditionResourceAdapter = new ConditionResourceAdapter(blobModel);
+ assert.deepStrictEqual(conditionResourceAdapter.exist, true);
+ assert.deepStrictEqual(conditionResourceAdapter.etag, "etag1");
+
+ blobModel.properties.lastModified.setMilliseconds(0);
+ assert.deepStrictEqual(
+ conditionResourceAdapter.lastModified,
+ blobModel.properties.lastModified
+ );
+ });
+
+ it("Should work with etag with quotes @loki @sql", () => {
+ const blobModel = {
+ properties: {
+ etag: '"etag1"',
+ lastModified: new Date()
+ }
+ } as ContainerModel;
+
+ const conditionResourceAdapter = new ConditionResourceAdapter(blobModel);
+ assert.deepStrictEqual(conditionResourceAdapter.exist, true);
+ assert.deepStrictEqual(conditionResourceAdapter.etag, "etag1");
+
+ blobModel.properties.lastModified.setMilliseconds(0);
+ assert.deepStrictEqual(
+ conditionResourceAdapter.lastModified,
+ blobModel.properties.lastModified
+ );
+ });
+});
+
+describe("ReadConditionalHeadersValidator for exist resource", () => {
+ it("Should return 412 preconditoin failed for failed if-match results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = { ifMatch: "etag1" };
+ const blobModel = {
+ properties: {
+ etag: "etag2",
+ lastModified: new Date()
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should not return 412 preconditoin failed for successful if-match results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = { ifMatch: "etag1" };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date()
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 304 Not Modified for failed if-none-match results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = { ifNoneMatch: 'etag0,"etag1"' };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date()
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getNotModified(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should not return 304 Not Modified for successful if-none-match results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = { ifNoneMatch: 'etag0,"etag3"' };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date()
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 304 Not Modified for failed if-modified-since results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2018/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getNotModified(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should return 304 Not Modified when if-modified-since same with lastModified @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2019/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getNotModified(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should not return 304 Not Modified for successful if-modified-since results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2020/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 412 preconditoin failed for failed if-unmodified-since results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifUnmodifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag2",
+ lastModified: new Date("2020/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should not return 412 preconditoin failed when if-unmodified-since same with lastModified @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifUnmodifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag2",
+ lastModified: new Date("2019/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should not return 412 preconditoin failed for successful if-unmodified-since results @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifUnmodifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag2",
+ lastModified: new Date("2018/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 200 OK when all conditions match @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1",
+ ifNoneMatch: "etag3",
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2019/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 412 Precondition Failed when if-none-match and if-unmodified-since fail among all conditions @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1",
+ ifNoneMatch: '"etag1"',
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2021/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should return 200 OK when if-none-match fails all conditions @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1",
+ ifNoneMatch: "etag1",
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2019/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 412 Precondition Failed when if-match and if-modified-since fail among all conditions @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1",
+ ifNoneMatch: '"etag2"',
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag0",
+ lastModified: new Date("2017/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should return 200 OK when if-modified-since fails all conditions @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1",
+ ifNoneMatch: "etag3",
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2017/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 304 Not Modified when if-none-match and if-modified-since fail @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifNoneMatch: '"etag1"',
+ ifModifiedSince: new Date("2018/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2017/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getNotModified(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+});
+
+describe("ReadConditionalHeadersValidator for unexist resource", () => {
+ it("Should return 412 Precondition Failed for any ifMatch @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1"
+ };
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(undefined)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should return 400 getUnsatisfiableCondition for if none-match value * @loki @sql", () => {
+ const validator = new ReadConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifNoneMatch: "*"
+ };
+
+ const expectedError = StorageErrorFactory.getUnsatisfiableCondition(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(undefined)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+});
+
+describe("WriteConditionalHeadersValidator for unexist resource", () => {
+ it("Should throw 400 Bad Request for invalid combinations conditional headers @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1",
+ ifNoneMatch: '"etag2"',
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+
+ const expectedError = StorageErrorFactory.getMultipleConditionHeadersNotSupported(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(undefined)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should throw 400 Bad Request for multi etags in ifMatch @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag1,etag2",
+ ifNoneMatch: '"etag2"',
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+
+ const expectedError = StorageErrorFactory.getMultipleConditionHeadersNotSupported(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(undefined)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should throw 400 Bad Request for multi etags in if-none-match @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag2",
+ ifNoneMatch: '"etag2",*',
+ ifModifiedSince: new Date("2018/01/01"),
+ ifUnmodifiedSince: new Date("2020/01/01")
+ };
+
+ const expectedError = StorageErrorFactory.getMultipleConditionHeadersNotSupported(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(undefined)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should throw 412 Precondition Failed for any values in if-match @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: "etag2"
+ };
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(undefined)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+});
+
+describe("WriteConditionalHeadersValidator for exist resource", () => {
+ it("Should return 200 for successful if-none-match and failed if-modified-since @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifNoneMatch: '"etag2"',
+ ifModifiedSince: new Date("2018/01/01")
+ };
+
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2017/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 200 for successful if-match and failed if-unmodified-since @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifMatch: '"etag1"',
+ ifUnmodifiedSince: new Date("2018/01/01")
+ };
+
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2019/01/01")
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 200 for if-unmodified-since equal with lastModified @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifUnmodifiedSince: new Date()
+ };
+
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: modifiedAccessConditions.ifUnmodifiedSince
+ }
+ } as BlobModel;
+
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ });
+
+ it("Should return 412 for if-modified-since equal with lastModifiedSince @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date()
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: modifiedAccessConditions.ifModifiedSince
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should return 412 for failed if-modified-since results @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifModifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2018/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+
+ it("Should return 412 for failed if-unmodified-since results @loki @sql", () => {
+ const validator = new WriteConditionalHeadersValidator();
+ const modifiedAccessConditions = {
+ ifUnmodifiedSince: new Date("2019/01/01")
+ };
+ const blobModel = {
+ properties: {
+ etag: "etag1",
+ lastModified: new Date("2020/01/01")
+ }
+ } as BlobModel;
+
+ const expectedError = StorageErrorFactory.getConditionNotMet(
+ context.contextId!
+ );
+ try {
+ validator.validate(
+ context,
+ new ConditionalHeadersAdapter(context, modifiedAccessConditions),
+ new ConditionResourceAdapter(blobModel)
+ );
+ } catch (error) {
+ assert.deepStrictEqual(error.statusCode, expectedError.statusCode);
+ assert.deepStrictEqual(
+ error.storageErrorCode,
+ expectedError.storageErrorCode
+ );
+ assert.deepStrictEqual(
+ error.storageErrorMessage,
+ expectedError.storageErrorMessage
+ );
+ return;
+ }
+
+ assert.fail();
+ });
+});
diff --git a/tests/blob/specialnaming.test.ts b/tests/blob/specialnaming.test.ts
index 5e4703d3d..52cdda49b 100644
--- a/tests/blob/specialnaming.test.ts
+++ b/tests/blob/specialnaming.test.ts
@@ -1,3 +1,5 @@
+import dns = require("dns");
+
import {
Aborter,
BlockBlobURL,
@@ -25,6 +27,8 @@ describe("SpecialNaming", () => {
const server = factory.createServer();
const baseURL = `http://${server.config.host}:${server.config.port}/devstoreaccount1`;
+ const productionStyleHostName = "devstoreaccount1.localhost"; // Use hosts file to make this resolve
+
const serviceURL = new ServiceURL(
baseURL,
StorageURL.newPipeline(
@@ -438,4 +442,56 @@ describe("SpecialNaming", () => {
);
assert.notDeepEqual(response.segment.blobItems.length, 0);
});
+
+ it(`Should work with production style URL when ${productionStyleHostName} is resolvable`, async () => {
+ await dns.promises.lookup(productionStyleHostName).then(
+ async lookupAddress => {
+ const baseURLProductionStyle = `http://${productionStyleHostName}:${server.config.port}`;
+ const serviceURLProductionStyle = new ServiceURL(
+ baseURLProductionStyle,
+ StorageURL.newPipeline(
+ new SharedKeyCredential(
+ EMULATOR_ACCOUNT_NAME,
+ EMULATOR_ACCOUNT_KEY
+ ),
+ {
+ retryOptions: { maxTries: 1 }
+ }
+ )
+ );
+ const containerURLProductionStyle = ContainerURL.fromServiceURL(
+ serviceURLProductionStyle,
+ containerName
+ );
+
+ const blobName: string = getUniqueName("myblob");
+ const blockBlobURL = BlockBlobURL.fromContainerURL(
+ containerURLProductionStyle,
+ blobName
+ );
+
+ await blockBlobURL.upload(Aborter.none, "ABC", 3);
+ const response = await containerURLProductionStyle.listBlobHierarchySegment(
+ Aborter.none,
+ "$",
+ undefined,
+ {
+ prefix: blobName
+ }
+ );
+ assert.notDeepEqual(response.segment.blobItems.length, 0);
+ },
+ () => {
+ // Cannot perform this test. We need devstoreaccount1.localhost to resolve to 127.0.0.1.
+ // On Linux, this should just work,
+ // On Windows, we can't spoof DNS record for specific process.
+ // So we have options of running our own DNS server (overkill),
+ // or editing hosts files (machine global operation; and requires running as admin).
+ // So skip the test case.
+ assert.ok(
+ `Skipping test case - it needs ${productionStyleHostName} to be resolvable`
+ );
+ }
+ );
+ });
});
diff --git a/tests/blob/utils.test.ts b/tests/blob/utils.test.ts
new file mode 100644
index 000000000..382f2ce02
--- /dev/null
+++ b/tests/blob/utils.test.ts
@@ -0,0 +1,56 @@
+import assert = require("assert");
+import { convertRawHeadersToMetadata } from "../../src/common/utils/utils";
+
+describe("Utils", () => {
+ it("convertRawHeadersToMetadata should work", () => {
+ // upper case, lower case keys/values
+ const metadata = convertRawHeadersToMetadata([
+ "x-ms-meta-Name1",
+ "Value",
+ "x-ms-meta-name2",
+ "234",
+ "x-ms-meta-name1",
+ "Value"
+ ]);
+ assert.deepStrictEqual(metadata, {
+ Name1: "Value",
+ name2: "234",
+ name1: "Value"
+ });
+ });
+
+ it("convertRawHeadersToMetadata should work with duplicated metadata", () => {
+ const metadata = convertRawHeadersToMetadata([
+ "x-ms-meta-name1",
+ "Value",
+ "x-ms-meta-name1",
+ "234"
+ ]);
+ assert.deepStrictEqual(metadata, {
+ name1: "Value,234"
+ });
+ });
+
+ it("convertRawHeadersToMetadata should work with empty metadata", () => {
+ const metadata = convertRawHeadersToMetadata([
+ "x-ms-meta-Name1",
+ "",
+ "x-ms-meta-name1",
+ "234"
+ ]);
+ assert.deepStrictEqual(metadata, {
+ Name1: "",
+ name1: "234"
+ });
+ });
+
+ it("convertRawHeadersToMetadata should work with empty raw headers", () => {
+ const metadata = convertRawHeadersToMetadata();
+ assert.deepStrictEqual(metadata, undefined);
+ });
+
+ it("convertRawHeadersToMetadata should work with empty raw headers array", () => {
+ const metadata = convertRawHeadersToMetadata([]);
+ assert.deepStrictEqual(metadata, undefined);
+ });
+});