@hot-updater/aws 0.35.11 → 0.36.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/dist/index.cjs CHANGED
@@ -1515,9 +1515,17 @@ function isPlatformDirectoryPrefix(prefix) {
1515
1515
  const lastSegment = getLastDirectorySegment(prefix);
1516
1516
  return lastSegment === "ios" || lastSegment === "android";
1517
1517
  }
1518
+ function isLegacyBundleStorageDirectoryPrefix(prefix) {
1519
+ const lastSegment = getLastDirectorySegment(prefix);
1520
+ return lastSegment !== void 0 && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(lastSegment);
1521
+ }
1518
1522
  function isUpdateJsonKey(key) {
1519
1523
  return key.endsWith(`${S3_DIRECTORY_DELIMITER}update.json`);
1520
1524
  }
1525
+ function isUuidChannelUpdateJsonKey(key, rootPrefix, uuidChannels) {
1526
+ const [channel, platform, target, fileName, ...rest] = getRelativeDirectoryPrefix(key, rootPrefix).split(S3_DIRECTORY_DELIMITER);
1527
+ return channel !== void 0 && uuidChannels.has(channel) && (platform === "ios" || platform === "android") && target !== void 0 && target.length > 0 && fileName === "update.json" && rest.length === 0;
1528
+ }
1521
1529
  async function mapWithConcurrency(items, concurrency, mapper) {
1522
1530
  const results = [];
1523
1531
  let nextIndex = 0;
@@ -1572,7 +1580,7 @@ async function uploadJsonToS3(client, bucket, key, data) {
1572
1580
  }
1573
1581
  async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1574
1582
  const normalizedRootPrefix = normalizeDirectoryPrefix(rootPrefix);
1575
- const listPrefix = async (currentPrefix) => {
1583
+ const listPrefix = async (currentPrefix, delimiter = S3_DIRECTORY_DELIMITER) => {
1576
1584
  let continuationToken;
1577
1585
  const keys = [];
1578
1586
  const commonPrefixes = /* @__PURE__ */ new Set();
@@ -1580,7 +1588,7 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1580
1588
  const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
1581
1589
  Bucket: bucketName,
1582
1590
  Prefix: currentPrefix,
1583
- Delimiter: S3_DIRECTORY_DELIMITER,
1591
+ Delimiter: delimiter ?? void 0,
1584
1592
  ContinuationToken: continuationToken
1585
1593
  }));
1586
1594
  const found = (response.Contents ?? []).map((item) => item.Key).filter((key) => !!key);
@@ -1597,7 +1605,12 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1597
1605
  const { commonPrefixes, keys } = await listPrefix(currentPrefix);
1598
1606
  const depth = getDirectoryDepth(currentPrefix, normalizedRootPrefix);
1599
1607
  if (depth >= 2) return [...keys.filter(isUpdateJsonKey), ...commonPrefixes.map((commonPrefix) => `${commonPrefix}update.json`)];
1600
- return (await mapWithConcurrency(depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix))).flat();
1608
+ const uuidPrefixes = depth === 0 ? commonPrefixes.filter(isLegacyBundleStorageDirectoryPrefix) : [];
1609
+ const uuidPrefixSet = new Set(uuidPrefixes);
1610
+ const nestedKeys = await mapWithConcurrency(depth === 0 ? commonPrefixes.filter((commonPrefix) => !uuidPrefixSet.has(commonPrefix)) : depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix));
1611
+ const uuidChannels = new Set(uuidPrefixes.map(getLastDirectorySegment).filter((segment) => !!segment));
1612
+ const uuidChannelKeys = (await mapWithConcurrency(Array.from(new Set(Array.from(uuidChannels, (channel) => `${normalizedRootPrefix}${channel[0]}`))), S3_LIST_OBJECTS_CONCURRENCY, async (scanPrefix) => (await listPrefix(scanPrefix, null)).keys)).flat().filter((key) => isUuidChannelUpdateJsonKey(key, normalizedRootPrefix, uuidChannels));
1613
+ return [...nestedKeys.flat(), ...uuidChannelKeys];
1601
1614
  };
1602
1615
  const normalizedPrefix = normalizeDirectoryPrefix(prefix);
1603
1616
  return Array.from(new Set(await collectUpdateJsonKeys(normalizedPrefix)));
@@ -1682,9 +1695,56 @@ const s3Storage = (0, _hot_updater_plugin_core.createUniversalStoragePlugin)({
1682
1695
  factory: (config) => {
1683
1696
  const { bucketName, ...s3Config } = config;
1684
1697
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
1685
- const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
1698
+ const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
1699
+ const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
1700
+ const getListPrefix = (prefix = "") => {
1701
+ const value = [normalizedBasePath, prefix.replace(/^\/+|\/+$/g, "")].filter(Boolean).join("/");
1702
+ return value ? `${value}/` : "";
1703
+ };
1704
+ const getRelativeKey = (key) => {
1705
+ if (!normalizedBasePath) return key;
1706
+ const basePrefix = `${normalizedBasePath}/`;
1707
+ return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
1708
+ };
1686
1709
  return {
1687
1710
  node: {
1711
+ async listObjects(prefix) {
1712
+ const objects = [];
1713
+ let continuationToken;
1714
+ do {
1715
+ const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
1716
+ Bucket: bucketName,
1717
+ ContinuationToken: continuationToken,
1718
+ Prefix: getListPrefix(prefix)
1719
+ }));
1720
+ for (const object of response.Contents ?? []) {
1721
+ if (!object.Key) continue;
1722
+ objects.push({
1723
+ key: getRelativeKey(object.Key),
1724
+ lastModifiedAt: object.LastModified,
1725
+ size: object.Size ?? 0,
1726
+ storageUri: `s3://${bucketName}/${object.Key}`
1727
+ });
1728
+ }
1729
+ continuationToken = response.NextContinuationToken;
1730
+ } while (continuationToken);
1731
+ return objects;
1732
+ },
1733
+ async deleteObjects(relativeKeys) {
1734
+ const keys = relativeKeys.map((key) => getStorageKey(key));
1735
+ for (let offset = 0; offset < keys.length; offset += 1e3) {
1736
+ const batch = keys.slice(offset, offset + 1e3);
1737
+ if (batch.length === 0) continue;
1738
+ const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
1739
+ Bucket: bucketName,
1740
+ Delete: {
1741
+ Objects: batch.map((Key) => ({ Key })),
1742
+ Quiet: true
1743
+ }
1744
+ }));
1745
+ if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
1746
+ }
1747
+ },
1688
1748
  async delete(storageUri) {
1689
1749
  const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "s3");
1690
1750
  if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
package/dist/index.mjs CHANGED
@@ -1490,9 +1490,17 @@ function isPlatformDirectoryPrefix(prefix) {
1490
1490
  const lastSegment = getLastDirectorySegment(prefix);
1491
1491
  return lastSegment === "ios" || lastSegment === "android";
1492
1492
  }
1493
+ function isLegacyBundleStorageDirectoryPrefix(prefix) {
1494
+ const lastSegment = getLastDirectorySegment(prefix);
1495
+ return lastSegment !== void 0 && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(lastSegment);
1496
+ }
1493
1497
  function isUpdateJsonKey(key) {
1494
1498
  return key.endsWith(`${S3_DIRECTORY_DELIMITER}update.json`);
1495
1499
  }
1500
+ function isUuidChannelUpdateJsonKey(key, rootPrefix, uuidChannels) {
1501
+ const [channel, platform, target, fileName, ...rest] = getRelativeDirectoryPrefix(key, rootPrefix).split(S3_DIRECTORY_DELIMITER);
1502
+ return channel !== void 0 && uuidChannels.has(channel) && (platform === "ios" || platform === "android") && target !== void 0 && target.length > 0 && fileName === "update.json" && rest.length === 0;
1503
+ }
1496
1504
  async function mapWithConcurrency(items, concurrency, mapper) {
1497
1505
  const results = [];
1498
1506
  let nextIndex = 0;
@@ -1547,7 +1555,7 @@ async function uploadJsonToS3(client, bucket, key, data) {
1547
1555
  }
1548
1556
  async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1549
1557
  const normalizedRootPrefix = normalizeDirectoryPrefix(rootPrefix);
1550
- const listPrefix = async (currentPrefix) => {
1558
+ const listPrefix = async (currentPrefix, delimiter = S3_DIRECTORY_DELIMITER) => {
1551
1559
  let continuationToken;
1552
1560
  const keys = [];
1553
1561
  const commonPrefixes = /* @__PURE__ */ new Set();
@@ -1555,7 +1563,7 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1555
1563
  const response = await client.send(new ListObjectsV2Command({
1556
1564
  Bucket: bucketName,
1557
1565
  Prefix: currentPrefix,
1558
- Delimiter: S3_DIRECTORY_DELIMITER,
1566
+ Delimiter: delimiter ?? void 0,
1559
1567
  ContinuationToken: continuationToken
1560
1568
  }));
1561
1569
  const found = (response.Contents ?? []).map((item) => item.Key).filter((key) => !!key);
@@ -1572,7 +1580,12 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1572
1580
  const { commonPrefixes, keys } = await listPrefix(currentPrefix);
1573
1581
  const depth = getDirectoryDepth(currentPrefix, normalizedRootPrefix);
1574
1582
  if (depth >= 2) return [...keys.filter(isUpdateJsonKey), ...commonPrefixes.map((commonPrefix) => `${commonPrefix}update.json`)];
1575
- return (await mapWithConcurrency(depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix))).flat();
1583
+ const uuidPrefixes = depth === 0 ? commonPrefixes.filter(isLegacyBundleStorageDirectoryPrefix) : [];
1584
+ const uuidPrefixSet = new Set(uuidPrefixes);
1585
+ const nestedKeys = await mapWithConcurrency(depth === 0 ? commonPrefixes.filter((commonPrefix) => !uuidPrefixSet.has(commonPrefix)) : depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix));
1586
+ const uuidChannels = new Set(uuidPrefixes.map(getLastDirectorySegment).filter((segment) => !!segment));
1587
+ const uuidChannelKeys = (await mapWithConcurrency(Array.from(new Set(Array.from(uuidChannels, (channel) => `${normalizedRootPrefix}${channel[0]}`))), S3_LIST_OBJECTS_CONCURRENCY, async (scanPrefix) => (await listPrefix(scanPrefix, null)).keys)).flat().filter((key) => isUuidChannelUpdateJsonKey(key, normalizedRootPrefix, uuidChannels));
1588
+ return [...nestedKeys.flat(), ...uuidChannelKeys];
1576
1589
  };
1577
1590
  const normalizedPrefix = normalizeDirectoryPrefix(prefix);
1578
1591
  return Array.from(new Set(await collectUpdateJsonKeys(normalizedPrefix)));
@@ -1657,9 +1670,56 @@ const s3Storage = createUniversalStoragePlugin({
1657
1670
  factory: (config) => {
1658
1671
  const { bucketName, ...s3Config } = config;
1659
1672
  const client = new S3Client(applyS3RuntimeAwsConfig(s3Config));
1660
- const getStorageKey = createStorageKeyBuilder(config.basePath);
1673
+ const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
1674
+ const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
1675
+ const getListPrefix = (prefix = "") => {
1676
+ const value = [normalizedBasePath, prefix.replace(/^\/+|\/+$/g, "")].filter(Boolean).join("/");
1677
+ return value ? `${value}/` : "";
1678
+ };
1679
+ const getRelativeKey = (key) => {
1680
+ if (!normalizedBasePath) return key;
1681
+ const basePrefix = `${normalizedBasePath}/`;
1682
+ return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
1683
+ };
1661
1684
  return {
1662
1685
  node: {
1686
+ async listObjects(prefix) {
1687
+ const objects = [];
1688
+ let continuationToken;
1689
+ do {
1690
+ const response = await client.send(new ListObjectsV2Command({
1691
+ Bucket: bucketName,
1692
+ ContinuationToken: continuationToken,
1693
+ Prefix: getListPrefix(prefix)
1694
+ }));
1695
+ for (const object of response.Contents ?? []) {
1696
+ if (!object.Key) continue;
1697
+ objects.push({
1698
+ key: getRelativeKey(object.Key),
1699
+ lastModifiedAt: object.LastModified,
1700
+ size: object.Size ?? 0,
1701
+ storageUri: `s3://${bucketName}/${object.Key}`
1702
+ });
1703
+ }
1704
+ continuationToken = response.NextContinuationToken;
1705
+ } while (continuationToken);
1706
+ return objects;
1707
+ },
1708
+ async deleteObjects(relativeKeys) {
1709
+ const keys = relativeKeys.map((key) => getStorageKey(key));
1710
+ for (let offset = 0; offset < keys.length; offset += 1e3) {
1711
+ const batch = keys.slice(offset, offset + 1e3);
1712
+ if (batch.length === 0) continue;
1713
+ const response = await client.send(new DeleteObjectsCommand({
1714
+ Bucket: bucketName,
1715
+ Delete: {
1716
+ Objects: batch.map((Key) => ({ Key })),
1717
+ Quiet: true
1718
+ }
1719
+ }));
1720
+ if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
1721
+ }
1722
+ },
1663
1723
  async delete(storageUri) {
1664
1724
  const { bucket, key } = parseStorageUri(storageUri, "s3");
1665
1725
  if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);