@pi-r/azure 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/index.js CHANGED
@@ -10,6 +10,7 @@ exports.setBucketWebsite = setBucketWebsite;
10
10
  exports.deleteObjects = deleteObjects;
11
11
  exports.deleteObjectsV2 = deleteObjectsV2;
12
12
  exports.deleteObjectsV3 = deleteObjectsV3;
13
+ exports.copyObject = copyObject;
13
14
  exports.executeQuery = executeQuery;
14
15
  exports.executeBatchQuery = executeBatchQuery;
15
16
  const path = require("node:path");
@@ -19,11 +20,15 @@ const identity = require("@azure/identity");
19
20
  const Cloud = require("@e-mc/cloud");
20
21
  const types_1 = require("@e-mc/types");
21
22
  const util_1 = require("@e-mc/cloud/util");
22
- function validateStorage(credential) {
23
+ function isEnvironmentCredential() {
23
24
  const env = process.env;
24
- if (credential.accountName && (credential.accountKey || credential.accountSas) || credential.connectionString || credential.sharedAccessSignature || env.AZURE_TENANT_ID && env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
25
+ return !!(env.AZURE_TENANT_ID && env.AZURE_CLIENT_ID && (env.AZURE_CLIENT_SECRET || env.AZURE_CLIENT_CERTIFICATE_PATH));
26
+ }
27
+ function validateStorage(credential) {
28
+ if (credential.accountName && (credential.accountKey || credential.accountSas) || credential.connectionString || credential.sharedAccessSignature || isEnvironmentCredential()) {
25
29
  return true;
26
30
  }
31
+ const env = process.env;
27
32
  return Cloud.enabled("process.env.apply") && !!((credential.connectionString = env.AZURE_STORAGE_CONNECTION_STRING) || (credential.sharedAccessSignature = env.AZURE_STORAGE_SAS_TOKEN) || (credential.accountName = env.AZURE_STORAGE_ACCOUNT) && (credential.accountKey = env.AZURE_STORAGE_KEY));
28
33
  }
29
34
  function validateDatabase(credential, data) {
@@ -59,6 +64,9 @@ function createStorageClient(credential) {
59
64
  else if (accountName && accountKey) {
60
65
  serviceCredential = new storage.StorageSharedKeyCredential(accountName, accountKey);
61
66
  }
67
+ else if (isEnvironmentCredential()) {
68
+ serviceCredential = new identity.EnvironmentCredential();
69
+ }
62
70
  else {
63
71
  serviceCredential = new identity.DefaultAzureCredential(credential.defaultAzureOptions);
64
72
  }
@@ -73,12 +81,15 @@ function createDatabaseClient(credential) {
73
81
  delete credential.tenantId;
74
82
  delete credential.clientId;
75
83
  }
84
+ else if (isEnvironmentCredential()) {
85
+ credential.aadCredentials = new identity.EnvironmentCredential();
86
+ }
76
87
  return new cosmos.CosmosClient(credential);
77
88
  }
78
- async function createBucket(credential, bucket, publicRead, service = 'azure') {
89
+ async function createBucket(credential, bucket, publicRead, service = "azure") {
79
90
  return createBucketV2.call(this, credential, bucket, publicRead ? 'blob' : 'container', undefined, service);
80
91
  }
81
- async function createBucketV2(credential, bucket, access, options, service = 'azure') {
92
+ async function createBucketV2(credential, bucket, access, options, service = "azure") {
82
93
  switch (access) {
83
94
  case 'blob':
84
95
  case 'container':
@@ -87,10 +98,10 @@ async function createBucketV2(credential, bucket, access, options, service = 'az
87
98
  access = undefined;
88
99
  break;
89
100
  }
90
- const containerClient = createStorageClient.call(this, credential).getContainerClient(bucket);
101
+ const containerClient = createStorageClient(credential).getContainerClient(bucket);
91
102
  const setAccessPolicy = async () => {
92
103
  return containerClient.setAccessPolicy(access).catch((err) => {
93
- this.formatMessage(64, service, ["Unable to configure container", bucket], err, { ...Cloud.LOG_CLOUD_WARN });
104
+ this.formatMessage(64, service, ["Unable to configure container", bucket], err, Cloud.optionsLogMessage('WARN'));
94
105
  });
95
106
  };
96
107
  if (await containerClient.exists().catch(() => false)) {
@@ -108,25 +119,25 @@ async function createBucketV2(credential, bucket, access, options, service = 'az
108
119
  .then(response => {
109
120
  const { requestId, errorCode } = response;
110
121
  if (errorCode) {
111
- this.formatMessage(64, service, ['Container created with errors', bucket], (0, types_1.errorMessage)("Error code", errorCode), { ...Cloud.LOG_CLOUD_WARN });
122
+ this.formatMessage(64, service, ['Container created with errors', bucket], (0, types_1.errorMessage)("Error code", errorCode), Cloud.optionsLogMessage('WARN'));
112
123
  }
113
124
  else {
114
- this.formatMessage(64, service, ['Container created', bucket], requestId, { ...Cloud.LOG_CLOUD_COMMAND });
125
+ this.formatMessage(64, service, ['Container created', bucket], requestId, Cloud.optionsLogMessage('COMMAND'));
115
126
  }
116
127
  return true;
117
128
  })
118
129
  .catch(async (err) => {
119
- if (err instanceof Error && err.code === 'ContainerAlreadyExists') {
130
+ if ((0, types_1.isErrorCode)(err, 'ContainerAlreadyExists')) {
120
131
  if (access) {
121
132
  await setAccessPolicy();
122
133
  }
123
134
  return true;
124
135
  }
125
- this.formatFail(64, service, ["Unable to create container", bucket], err, { ...Cloud.LOG_CLOUD_FAIL });
136
+ this.formatFail(64, service, ["Unable to create container", bucket], err, Cloud.optionsLogMessage('FAIL'));
126
137
  return false;
127
138
  });
128
139
  }
129
- async function setBucketWebsite(credential, bucket, options, service = 'azure') {
140
+ async function setBucketWebsite(credential, bucket, options, service = "azure") {
130
141
  const staticWebsite = { enabled: true };
131
142
  const { indexPage, indexPath, errorPath } = options;
132
143
  if ((0, types_1.isString)(indexPage)) {
@@ -138,19 +149,19 @@ async function setBucketWebsite(credential, bucket, options, service = 'azure')
138
149
  if ((0, types_1.isString)(errorPath)) {
139
150
  staticWebsite.errorDocument404Path = Cloud.joinPath(bucket, errorPath);
140
151
  }
141
- return createStorageClient.call(this, credential).setProperties({ staticWebsite })
152
+ return createStorageClient(credential).setProperties({ staticWebsite })
142
153
  .then(response => {
143
154
  const { requestId, errorCode } = response;
144
155
  if (errorCode) {
145
- this.formatMessage(64, service, ["Bucket website configured" + ' with errors', bucket], (0, types_1.errorMessage)("Error code", errorCode), { ...Cloud.LOG_CLOUD_WARN });
156
+ this.formatMessage(64, service, ["Bucket website configured" + ' with errors', bucket], (0, types_1.errorMessage)("Error code", errorCode), Cloud.optionsLogMessage('WARN'));
146
157
  }
147
158
  else {
148
- this.formatMessage(64, service, ["Bucket website configured", bucket], requestId, { ...Cloud.LOG_CLOUD_COMMAND });
159
+ this.formatMessage(64, service, ["Bucket website configured", bucket], requestId, Cloud.optionsLogMessage('COMMAND'));
149
160
  }
150
161
  return true;
151
162
  })
152
163
  .catch((err) => {
153
- this.formatFail(64, service, ["Unable to set bucket website", bucket], err, { ...Cloud.LOG_CLOUD_FAIL, fatal: false });
164
+ this.formatFail(64, service, ["Unable to set bucket website", bucket], err, Cloud.optionsLogMessage('FAIL', { fatal: false }));
154
165
  return false;
155
166
  });
156
167
  }
@@ -160,8 +171,8 @@ async function deleteObjects(credential, bucket, service, sdk, recursive = true)
160
171
  async function deleteObjectsV2(credential, bucket, recursive = true, service) {
161
172
  return deleteObjectsV3.call(this, credential, bucket, { recursive }, service);
162
173
  }
163
- async function deleteObjectsV3(credential, bucket, options = {}, service = 'azure') {
164
- const containerClient = createStorageClient.call(this, credential).getContainerClient(bucket);
174
+ async function deleteObjectsV3(credential, bucket, options = {}, service = "azure") {
175
+ const containerClient = createStorageClient(credential).getContainerClient(bucket);
165
176
  if (await containerClient.exists().catch(() => false)) {
166
177
  try {
167
178
  options = { ...options };
@@ -170,6 +181,7 @@ async function deleteObjectsV3(credential, bucket, options = {}, service = 'azur
170
181
  recursive = options.recursive;
171
182
  delete options.recursive;
172
183
  }
184
+ options.abortSignal = this.signal;
173
185
  options.includeUncommitedBlobs ??= true;
174
186
  const tasks = [];
175
187
  let fileCount = 0;
@@ -182,14 +194,32 @@ async function deleteObjectsV3(credential, bucket, options = {}, service = 'azur
182
194
  }
183
195
  fileCount = tasks.length;
184
196
  await this.allSettled(tasks, ["Unable to delete blob", bucket], 64).then(() => {
185
- this.formatMessage(64, service, ['Container emptied' + (recursive ? ' (recursive)' : ''), bucket], fileCount + ' files', { ...Cloud.LOG_CLOUD_COMMAND });
197
+ this.formatMessage(64, service, ['Container emptied' + (recursive ? ' (recursive)' : ''), bucket], fileCount + ' files', Cloud.optionsLogMessage('COMMAND'));
186
198
  });
187
199
  }
188
200
  catch (err) {
189
- this.formatFail(64, service, ["Unable to list bucket", bucket], err, { ...Cloud.LOG_CLOUD_FAIL, fatal: false });
201
+ this.formatFail(64, service, ["Unable to list bucket", bucket], err, Cloud.optionsLogMessage('FAIL', { fatal: false }));
190
202
  }
191
203
  }
192
204
  }
205
+ async function copyObject(credential, bucketSource, keySource, bucket, key, options = {}, service = "azure") {
206
+ const storageClient = createStorageClient(credential);
207
+ const client = storageClient.getContainerClient(bucketSource).getBlobClient(keySource);
208
+ const destination = storageClient.getContainerClient(bucket).getBlobClient(key);
209
+ const poller = await destination.beginCopyFromURL(client.url, { ...options, abortSignal: this.signal });
210
+ return poller.pollUntilDone()
211
+ .then(response => {
212
+ if (response.errorCode) {
213
+ this.formatMessage(64, service, ["Copy success" + ' with errors', bucket], (0, types_1.errorMessage)("Error code", response.errorCode, bucketSource), Cloud.optionsLogMessage('WARN'));
214
+ }
215
+ else {
216
+ this.formatMessage(64, service, ["Copy success", bucket], bucketSource + '/' + keySource, Cloud.optionsLogMessage('COMMAND'));
217
+ }
218
+ })
219
+ .catch((err) => {
220
+ this.formatFail(64, service, ["Copy failed", bucket + '/' + key], err, Cloud.optionsLogMessage('FAIL', { fatal: false }));
221
+ });
222
+ }
193
223
  async function executeQuery(credential, data, sessionKey) {
194
224
  return (await executeBatchQuery.call(this, credential, [data], sessionKey))[0] || [];
195
225
  }
@@ -200,7 +230,7 @@ async function executeBatchQuery(credential, batch, sessionKey) {
200
230
  const cacheValue = { value: this.valueOfKey(credential, 'cache'), sessionKey };
201
231
  let client;
202
232
  const createClient = (name, table) => {
203
- client ||= createDatabaseClient.call(this, credential);
233
+ client ||= createDatabaseClient(credential);
204
234
  return client.database(name).container(table);
205
235
  };
206
236
  const attachSignal = (opts) => {
package/download/index.js CHANGED
@@ -2,44 +2,55 @@
2
2
  const path = require("node:path");
3
3
  const Cloud = require("@e-mc/cloud");
4
4
  const types_1 = require("@e-mc/types");
5
+ const util_1 = require("@e-mc/cloud/util");
5
6
  const client_1 = require("@pi-r/azure");
6
- function download(credential, service = 'azure') {
7
- const blobServiceClient = client_1.createStorageClient.call(this, credential);
7
+ function download(credential, service = "azure") {
8
+ const blobServiceClient = (0, client_1.createStorageClient)(credential);
8
9
  return (data, callback) => {
9
- const { bucket: Bucket, download: target } = data;
10
- const Key = target.keyname || target.filename;
11
- if (!Bucket || !Key) {
12
- callback((0, types_1.errorValue)('Missing property', !Bucket ? 'Container' : 'Name'));
10
+ const { bucket, download: target } = data;
11
+ const key = target.keyname || target.filename;
12
+ if (!bucket || !key) {
13
+ callback((0, types_1.errorValue)('Missing property', !bucket ? 'Container' : 'Name'));
13
14
  return;
14
15
  }
15
16
  const { flags = 0 } = target;
16
- const containerClient = blobServiceClient.getContainerClient(Bucket);
17
- const blobClient = containerClient.getBlockBlobClient(Key);
17
+ const containerClient = blobServiceClient.getContainerClient(bucket);
18
+ const blobClient = containerClient.getBlockBlobClient(key);
18
19
  const blockSize = (flags & 4) && (0, types_1.alignSize)(target.chunkSize, 1024) || 0;
19
20
  const tempDir = blockSize >= 2097152000 ? (0, types_1.getTempDir)(true, service) : null;
20
21
  let diskClient, destination;
21
22
  if (tempDir) {
22
- destination = path.join(tempDir, path.basename(target.filename || Key));
23
+ destination = path.join(tempDir, path.basename(target.filename || key));
23
24
  diskClient = blobClient.downloadToFile(destination, 0, undefined, { snapshot: target.versionId, abortSignal: this.signal });
24
25
  }
25
26
  else {
26
27
  diskClient = blobClient.downloadToBuffer(0, 0, { ...target.options, blockSize, concurrency: blockSize > 0 ? target.chunkLimit : undefined, abortSignal: this.signal });
27
28
  }
28
29
  diskClient
29
- .then((value) => {
30
+ .then(async (value) => {
30
31
  callback(null, destination || value);
32
+ const copyTo = (0, util_1.intoArray)(target.copyObject);
33
+ if (copyTo) {
34
+ const tasks = [];
35
+ for (const { bucket: bucketName, pathname, filename, options } of copyTo) {
36
+ const keyObject = filename ? Cloud.joinPath(pathname, filename, true) : key;
37
+ tasks.push(client_1.copyObject
38
+ .call(this, credential, bucket, key, bucketName, keyObject, options)
39
+ .catch((0, util_1.createErrorHandler)(this, service, bucket)));
40
+ }
41
+ await Promise.all(tasks);
42
+ }
31
43
  const deleteObject = target.deleteObject;
32
44
  if (deleteObject) {
33
- const location = Cloud.joinPath(Bucket, Key);
34
- containerClient.delete((0, types_1.isPlainObject)(deleteObject) ? (deleteObject.abortSignal = this.signal, deleteObject) : { abortSignal: this.signal })
45
+ const location = Cloud.joinPath(bucket, key);
46
+ containerClient.delete((0, types_1.isPlainObject)(deleteObject) ? { ...deleteObject, abortSignal: this.signal } : { abortSignal: this.signal })
35
47
  .then(() => {
36
- this.formatMessage(64, service, "Delete success", location, { ...Cloud.LOG_CLOUD_DELETE });
48
+ this.formatMessage(64, service, "Delete success", location, Cloud.optionsLogMessage('DELETE'));
37
49
  })
38
50
  .catch((err) => {
39
- if (err instanceof Error && err.code === 'BlobNotFound') {
40
- return;
51
+ if (!(0, types_1.isErrorCode)(err, 'BlobNotFound')) {
52
+ this.formatFail(64, service, ["Delete failed", location], err, Cloud.optionsLogMessage('FAIL', { fatal: !!target.active }));
41
53
  }
42
- this.formatFail(64, service, ["Delete failed", location], err, { ...Cloud.LOG_CLOUD_FAIL, fatal: !!target.active });
43
54
  });
44
55
  }
45
56
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-r/azure",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "Azure cloud functions for E-mc.",
5
5
  "main": "client/index.js",
6
6
  "publishConfig": {
@@ -19,11 +19,11 @@
19
19
  "license": "MIT",
20
20
  "homepage": "https://github.com/anpham6/pi-r#readme",
21
21
  "dependencies": {
22
- "@e-mc/cloud": "^0.12.5",
23
- "@e-mc/module": "^0.12.5",
24
- "@e-mc/types": "^0.12.5",
25
- "@azure/cosmos": "^4.5.0",
22
+ "@e-mc/cloud": "^0.13.0",
23
+ "@e-mc/module": "^0.13.0",
24
+ "@e-mc/types": "^0.13.0",
25
+ "@azure/cosmos": "^4.6.0",
26
26
  "@azure/identity": "*",
27
- "@azure/storage-blob": "^12.28.0"
27
+ "@azure/storage-blob": "^12.29.1"
28
28
  }
29
29
  }
package/upload/index.js CHANGED
@@ -9,43 +9,33 @@ const client_1 = require("@pi-r/azure");
9
9
  const BUCKET_SESSION = new Set();
10
10
  const BUCKET_RESPONSE = {};
11
11
  const getBucketKey = (credential, bucket, acl = '') => Cloud.asString(credential, true) + bucket + '_' + acl;
12
- function upload(credential, service = 'azure') {
13
- const blobServiceClient = client_1.createStorageClient.call(this, credential);
12
+ function upload(credential, service = "azure") {
13
+ const blobServiceClient = (0, client_1.createStorageClient)(credential);
14
14
  return async (data, callback) => {
15
15
  const { bucket, localUri } = data;
16
- const { pathname = '', flags = 0, fileGroup, contentType, metadata, tags, endpoint, admin = {}, overwrite, options } = data.upload;
16
+ const { pathname, flags = 0, fileGroup, contentType, metadata, tags, endpoint, admin = {}, overwrite, options } = data.upload;
17
17
  const containerClient = blobServiceClient.getContainerClient(bucket);
18
18
  let filename = data.upload.filename || path.basename(localUri), bucketKey;
19
- const cleanup = () => {
19
+ const complete = (err, url) => {
20
20
  BUCKET_SESSION.delete(bucket);
21
21
  if (bucketKey) {
22
22
  delete BUCKET_RESPONSE[bucketKey];
23
23
  }
24
+ callback(err, url);
24
25
  };
25
- const errorResponse = (err) => {
26
- cleanup();
27
- callback(err);
28
- };
29
- const addLog = (err) => {
30
- if (err instanceof Error) {
31
- this.addLog(this.statusType.WARN, err, service, bucket);
32
- }
33
- };
26
+ const addLog = (0, util_1.createErrorHandler)(this, bucket, service);
34
27
  const configBucket = admin.configBucket;
35
28
  if (!BUCKET_SESSION.has(bucket)) {
36
29
  const bucketAcl = admin.publicRead ? 'blob' : admin.acl;
37
30
  const response = BUCKET_RESPONSE[bucketKey = getBucketKey(credential, bucket, bucketAcl)] ||= client_1.createBucketV2.call(this, credential, bucket, bucketAcl, configBucket?.create);
38
31
  if (!await response) {
39
- errorResponse(null);
32
+ complete(null);
40
33
  return;
41
34
  }
42
35
  BUCKET_SESSION.add(bucket);
43
36
  }
44
- let retentionPolicy = configBucket?.retentionPolicy;
37
+ const retentionPolicy = (0, util_1.intoArray)(configBucket?.retentionPolicy);
45
38
  if (retentionPolicy) {
46
- if (!Array.isArray(retentionPolicy)) {
47
- retentionPolicy = [retentionPolicy];
48
- }
49
39
  try {
50
40
  if (this.hasCoerce(service, 'options')) {
51
41
  for (const policy of retentionPolicy) {
@@ -58,7 +48,7 @@ function upload(credential, service = 'azure') {
58
48
  containerClient.setAccessPolicy('container', retentionPolicy)
59
49
  .then(() => {
60
50
  const { expiresOn, startsOn, permissions } = retentionPolicy[0].accessPolicy;
61
- this.formatMessage(64, service, ["Bucket configured" + ' (retention policy)', bucket], expiresOn ? expiresOn.toLocaleString() + ' (expires)' : startsOn ? startsOn.toLocaleString() + ' (starts)' : permissions, { ...Cloud.LOG_CLOUD_COMMAND });
51
+ this.formatMessage(64, service, ["Bucket configured" + ' (retention policy)', bucket], expiresOn ? expiresOn.toLocaleString() + ' (expires)' : startsOn ? startsOn.toLocaleString() + ' (starts)' : permissions, Cloud.optionsLogMessage('COMMAND'));
62
52
  })
63
53
  .catch(addLog);
64
54
  }
@@ -74,7 +64,7 @@ function upload(credential, service = 'azure') {
74
64
  break;
75
65
  }
76
66
  }
77
- const name = pathname ? Cloud.joinPath(pathname, filename) : filename;
67
+ const name = Cloud.joinPath(pathname, filename, true);
78
68
  for await (const blob of containerClient.listBlobsFlat({ includeUncommitedBlobs: true })) {
79
69
  if (blob.name === name) {
80
70
  exists = true;
@@ -83,11 +73,11 @@ function upload(credential, service = 'azure') {
83
73
  }
84
74
  } while (exists && ++i);
85
75
  if (i > 0) {
86
- this.formatMessage(64, service, ["File renamed", current], filename, { ...Cloud.LOG_CLOUD_WARN });
76
+ this.formatMessage(64, service, ["File renamed", current], filename, Cloud.optionsLogMessage('WARN'));
87
77
  }
88
78
  }
89
79
  catch (err) {
90
- errorResponse(err);
80
+ complete(err);
91
81
  return;
92
82
  }
93
83
  }
@@ -112,7 +102,7 @@ function upload(credential, service = 'azure') {
112
102
  }
113
103
  }
114
104
  catch (err) {
115
- errorResponse(err);
105
+ complete(err);
116
106
  return;
117
107
  }
118
108
  }
@@ -140,11 +130,11 @@ function upload(credential, service = 'azure') {
140
130
  const first = i === 0;
141
131
  if (this.aborted) {
142
132
  if (first) {
143
- errorResponse((0, types_1.createAbortError)());
133
+ complete((0, types_1.createAbortError)());
144
134
  }
145
135
  return;
146
136
  }
147
- const blobName = Cloud.joinPath(pathname, Key[i]);
137
+ const blobName = Cloud.joinPath(pathname, Key[i], true);
148
138
  const params = { ...options, abortSignal: this.signal };
149
139
  const headers = params.blobHTTPHeaders ||= {};
150
140
  if (first) {
@@ -181,15 +171,25 @@ function upload(credential, service = 'azure') {
181
171
  .then(result => {
182
172
  let requestUrl = result._response.request.url;
183
173
  const url = endpoint ? Cloud.joinPath(endpoint, blobName) : Cloud.isFile(requestUrl = decodeURIComponent(requestUrl), 'http/s') ? requestUrl : Cloud.joinPath(`https://${credential.accountName}.blob.core.windows.net`, bucket, blobName);
184
- this.formatMessage(64, service, "Upload success", url, { ...Cloud.LOG_CLOUD_UPLOAD });
174
+ this.formatMessage(64, service, "Upload success", url, Cloud.optionsLogMessage('UPLOAD'));
185
175
  if (first) {
186
- cleanup();
187
- callback(null, url);
176
+ const copyTo = (0, util_1.intoArray)(data.upload.copyObject);
177
+ if (copyTo) {
178
+ const tasks = [];
179
+ for (const { bucket: bucketName, pathname: pathObject = pathname, filename: fileObject, options: copyOptions } of copyTo) {
180
+ const keyObject = fileObject ? Cloud.joinPath(pathObject, fileObject, true) : blobName;
181
+ tasks.push(client_1.copyObject
182
+ .call(this, credential, bucket, blobName, bucketName, keyObject, copyOptions)
183
+ .catch(addLog));
184
+ }
185
+ void Promise.all(tasks);
186
+ }
187
+ complete(null, url);
188
188
  }
189
189
  })
190
190
  .catch((err) => {
191
191
  if (first) {
192
- errorResponse(err);
192
+ complete(err);
193
193
  }
194
194
  else {
195
195
  addLog(err);