@web-portal/core-infrastructure 1.0.1 → 1.0.2

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.
@@ -1687,6 +1687,9 @@ class FileGCSService {
1687
1687
  getSignedLink(filePath) {
1688
1688
  return this._apiService.post(`${this.baseUrl}/getSignedLink?filePath=${filePath}`);
1689
1689
  }
1690
+ getPresignedLink(fileName) {
1691
+ return this._apiService.post(`${this.baseUrl}/getPresignedLink?fileName=${fileName}`);
1692
+ }
1690
1693
  attachFile(file) {
1691
1694
  const formData = new FormData();
1692
1695
  formData.append("file", file);
@@ -1705,6 +1708,195 @@ class FileGCSService {
1705
1708
  });
1706
1709
  return firstValueFrom(this._http.request(req));
1707
1710
  }
1711
+ /**
1712
+ * Build an Error object but keep HTTP-related metadata for callers that need to branch by status.
1713
+ * Note: We intentionally do NOT change business flow; we only enrich the error instance.
1714
+ */
1715
+ buildHttpError(message, meta) {
1716
+ const err = new Error(message);
1717
+ if (meta?.status !== undefined)
1718
+ err.status = meta.status;
1719
+ if (meta?.statusText !== undefined)
1720
+ err.statusText = meta.statusText;
1721
+ if (meta?.url !== undefined)
1722
+ err.url = meta.url;
1723
+ if (meta?.cause !== undefined)
1724
+ err.cause = meta.cause;
1725
+ return err;
1726
+ }
1727
+ uploadFile(file) {
1728
+ return new Observable((observer) => {
1729
+ let xhr = null;
1730
+ // Tạo tên file unique để tránh trùng tên khi upload nhiều lần
1731
+ const uniqueFileName = this.generateUniqueFileName(file.name);
1732
+ firstValueFrom(this.getPresignedLink(uniqueFileName))
1733
+ .then((presignedData) => {
1734
+ if (!presignedData) {
1735
+ throw new Error("Không nhận được presigned link từ server");
1736
+ }
1737
+ const presignedURL = presignedData.PreSignedURL ||
1738
+ presignedData.preSignedURL ||
1739
+ presignedData.url;
1740
+ const filePath = presignedData.FilePath || presignedData.filePath;
1741
+ const absoluteURL = presignedData.AbsoluteURL || presignedData.absoluteURL;
1742
+ if (!presignedURL) {
1743
+ throw new Error("Presigned URL không hợp lệ");
1744
+ }
1745
+ const contentType = presignedData.ContentType ||
1746
+ presignedData.contentType ||
1747
+ file.type ||
1748
+ "application/octet-stream";
1749
+ xhr = new XMLHttpRequest();
1750
+ const cleanup = () => {
1751
+ if (xhr) {
1752
+ xhr.upload.removeEventListener("progress", progressHandler);
1753
+ xhr.removeEventListener("load", loadHandler);
1754
+ xhr.removeEventListener("error", errorHandler);
1755
+ xhr.removeEventListener("timeout", timeoutHandler);
1756
+ xhr.abort();
1757
+ xhr = null;
1758
+ }
1759
+ };
1760
+ const progressHandler = (_event) => {
1761
+ // No-op: progress not consumed by current callers
1762
+ };
1763
+ const loadHandler = async () => {
1764
+ if (xhr && xhr.status === 200) {
1765
+ try {
1766
+ const finalUrl = absoluteURL || presignedURL;
1767
+ const result = this.buildResult(file, filePath, finalUrl);
1768
+ cleanup();
1769
+ observer.next(result);
1770
+ observer.complete();
1771
+ }
1772
+ catch (err) {
1773
+ cleanup();
1774
+ const msg = err?.message ||
1775
+ "Upload thất bại. Vui lòng thử lại.";
1776
+ observer.error(this.buildHttpError(msg, {
1777
+ status: xhr?.status,
1778
+ statusText: xhr?.statusText,
1779
+ url: presignedURL,
1780
+ cause: err,
1781
+ }));
1782
+ }
1783
+ }
1784
+ else if (xhr) {
1785
+ const errorMessage = `Upload file thất bại: ${xhr.statusText} (${xhr.status})`;
1786
+ cleanup();
1787
+ observer.error(this.buildHttpError(errorMessage, {
1788
+ status: xhr.status,
1789
+ statusText: xhr.statusText,
1790
+ url: presignedURL,
1791
+ }));
1792
+ }
1793
+ };
1794
+ const errorHandler = () => {
1795
+ if (!xhr)
1796
+ return;
1797
+ let userMessage = "Upload file thất bại";
1798
+ if (xhr.status === 0) {
1799
+ userMessage =
1800
+ "Không thể kết nối đến máy chủ lưu trữ. Máy chủ không phản hồi.";
1801
+ }
1802
+ else if (xhr.status >= 500) {
1803
+ userMessage = "Lỗi máy chủ. Vui lòng thử lại sau.";
1804
+ }
1805
+ else if (xhr.status >= 400) {
1806
+ userMessage = `Lỗi yêu cầu: ${xhr.statusText}`;
1807
+ }
1808
+ cleanup();
1809
+ observer.error(this.buildHttpError(userMessage, {
1810
+ status: xhr.status,
1811
+ statusText: xhr.statusText,
1812
+ url: presignedURL,
1813
+ }));
1814
+ };
1815
+ const timeoutHandler = () => {
1816
+ cleanup();
1817
+ observer.error(this.buildHttpError("Kết nối đến máy chủ hết thời gian. Vui lòng thử lại.", {
1818
+ status: xhr?.status,
1819
+ statusText: xhr?.statusText,
1820
+ url: presignedURL,
1821
+ }));
1822
+ };
1823
+ xhr.upload.addEventListener("progress", progressHandler);
1824
+ xhr.addEventListener("load", loadHandler);
1825
+ xhr.addEventListener("error", errorHandler);
1826
+ xhr.addEventListener("timeout", timeoutHandler);
1827
+ xhr.timeout = 60000 * 5;
1828
+ xhr.open("PUT", presignedURL, true);
1829
+ xhr.setRequestHeader("Content-Type", contentType);
1830
+ xhr.send(file);
1831
+ })
1832
+ .catch((error) => {
1833
+ if (xhr) {
1834
+ xhr.abort();
1835
+ xhr = null;
1836
+ }
1837
+ let userMessage = "Upload file thất bại";
1838
+ if (error.errorMessage) {
1839
+ userMessage = error.errorMessage;
1840
+ }
1841
+ else if (error.code === "ECONNABORTED" ||
1842
+ error.message?.includes("timeout")) {
1843
+ userMessage =
1844
+ "Kết nối đến máy chủ hết thời gian. Vui lòng thử lại.";
1845
+ }
1846
+ else if (error.message?.includes("failed to respond") ||
1847
+ error.message?.includes("Network Error")) {
1848
+ userMessage =
1849
+ "Không thể kết nối đến máy chủ lưu trữ. Máy chủ không phản hồi.";
1850
+ }
1851
+ else if (error.message) {
1852
+ userMessage = `Lỗi kết nối: ${error.message}`;
1853
+ }
1854
+ else if (error.error?.message) {
1855
+ userMessage = error.error.message;
1856
+ }
1857
+ observer.error(this.buildHttpError(userMessage, {
1858
+ status: error?.status,
1859
+ statusText: error?.statusText,
1860
+ url: error?.url,
1861
+ cause: error,
1862
+ }));
1863
+ });
1864
+ return () => {
1865
+ if (xhr) {
1866
+ xhr.abort();
1867
+ xhr = null;
1868
+ }
1869
+ };
1870
+ });
1871
+ }
1872
+ /**
1873
+ * Tạo tên file unique bằng cách thêm timestamp và random string
1874
+ * Format: originalname-timestamp-random.ext
1875
+ */
1876
+ generateUniqueFileName(originalFileName) {
1877
+ const timestamp = Date.now();
1878
+ const randomString = Math.random().toString(36).substring(2, 9);
1879
+ // Tách tên file và extension
1880
+ const lastDotIndex = originalFileName.lastIndexOf(".");
1881
+ if (lastDotIndex === -1) {
1882
+ // Không có extension
1883
+ return `${originalFileName}-${timestamp}-${randomString}`;
1884
+ }
1885
+ const nameWithoutExt = originalFileName.substring(0, lastDotIndex);
1886
+ const extension = originalFileName.substring(lastDotIndex);
1887
+ return `${nameWithoutExt}-${timestamp}-${randomString}${extension}`;
1888
+ }
1889
+ extractFileName(filePath) {
1890
+ return filePath.split("/").pop() || "";
1891
+ }
1892
+ buildResult(file, filePath, url) {
1893
+ return {
1894
+ url,
1895
+ filePath,
1896
+ fileName: this.extractFileName(filePath) || file.name,
1897
+ originalName: file.name,
1898
+ };
1899
+ }
1708
1900
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileGCSService, deps: [{ token: i1$2.HttpClient }, { token: ApiService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1709
1901
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileGCSService, providedIn: "root" }); }
1710
1902
  }
@@ -1722,6 +1914,9 @@ class FileAIA_GCSService {
1722
1914
  getSignedLink(filePath) {
1723
1915
  return this._apiService.post(`${this.baseUrl}/getSignedLink?filePath=${filePath}`);
1724
1916
  }
1917
+ getPresignedLink(fileName) {
1918
+ return this._apiService.post(`${this.baseUrl}/getPresignedLink?fileName=${fileName}`);
1919
+ }
1725
1920
  attachFile(file) {
1726
1921
  const formData = new FormData();
1727
1922
  formData.append("file", file);
@@ -1740,6 +1935,195 @@ class FileAIA_GCSService {
1740
1935
  });
1741
1936
  return firstValueFrom(this._http.request(req));
1742
1937
  }
1938
+ /**
1939
+ * Build an Error object but keep HTTP-related metadata for callers that need to branch by status.
1940
+ * Note: We intentionally do NOT change business flow; we only enrich the error instance.
1941
+ */
1942
+ buildHttpError(message, meta) {
1943
+ const err = new Error(message);
1944
+ if (meta?.status !== undefined)
1945
+ err.status = meta.status;
1946
+ if (meta?.statusText !== undefined)
1947
+ err.statusText = meta.statusText;
1948
+ if (meta?.url !== undefined)
1949
+ err.url = meta.url;
1950
+ if (meta?.cause !== undefined)
1951
+ err.cause = meta.cause;
1952
+ return err;
1953
+ }
1954
+ uploadFile(file) {
1955
+ return new Observable((observer) => {
1956
+ let xhr = null;
1957
+ // Tạo tên file unique để tránh trùng tên khi upload nhiều lần
1958
+ const uniqueFileName = this.generateUniqueFileName(file.name);
1959
+ firstValueFrom(this.getPresignedLink(uniqueFileName))
1960
+ .then((presignedData) => {
1961
+ if (!presignedData) {
1962
+ throw new Error("Không nhận được presigned link từ server");
1963
+ }
1964
+ const presignedURL = presignedData.PreSignedURL ||
1965
+ presignedData.preSignedURL ||
1966
+ presignedData.url;
1967
+ const filePath = presignedData.FilePath || presignedData.filePath;
1968
+ const absoluteURL = presignedData.AbsoluteURL || presignedData.absoluteURL;
1969
+ if (!presignedURL) {
1970
+ throw new Error("Presigned URL không hợp lệ");
1971
+ }
1972
+ const contentType = presignedData.ContentType ||
1973
+ presignedData.contentType ||
1974
+ file.type ||
1975
+ "application/octet-stream";
1976
+ xhr = new XMLHttpRequest();
1977
+ const cleanup = () => {
1978
+ if (xhr) {
1979
+ xhr.upload.removeEventListener("progress", progressHandler);
1980
+ xhr.removeEventListener("load", loadHandler);
1981
+ xhr.removeEventListener("error", errorHandler);
1982
+ xhr.removeEventListener("timeout", timeoutHandler);
1983
+ xhr.abort();
1984
+ xhr = null;
1985
+ }
1986
+ };
1987
+ const progressHandler = (_event) => {
1988
+ // No-op: progress not consumed by current callers
1989
+ };
1990
+ const loadHandler = async () => {
1991
+ if (xhr && xhr.status === 200) {
1992
+ try {
1993
+ const finalUrl = absoluteURL || presignedURL;
1994
+ const result = this.buildResult(file, filePath, finalUrl);
1995
+ cleanup();
1996
+ observer.next(result);
1997
+ observer.complete();
1998
+ }
1999
+ catch (err) {
2000
+ cleanup();
2001
+ const msg = err?.message ||
2002
+ "Upload thất bại. Vui lòng thử lại.";
2003
+ observer.error(this.buildHttpError(msg, {
2004
+ status: xhr?.status,
2005
+ statusText: xhr?.statusText,
2006
+ url: presignedURL,
2007
+ cause: err,
2008
+ }));
2009
+ }
2010
+ }
2011
+ else if (xhr) {
2012
+ const errorMessage = `Upload file thất bại: ${xhr.statusText} (${xhr.status})`;
2013
+ cleanup();
2014
+ observer.error(this.buildHttpError(errorMessage, {
2015
+ status: xhr.status,
2016
+ statusText: xhr.statusText,
2017
+ url: presignedURL,
2018
+ }));
2019
+ }
2020
+ };
2021
+ const errorHandler = () => {
2022
+ if (!xhr)
2023
+ return;
2024
+ let userMessage = "Upload file thất bại";
2025
+ if (xhr.status === 0) {
2026
+ userMessage =
2027
+ "Không thể kết nối đến máy chủ lưu trữ. Máy chủ không phản hồi.";
2028
+ }
2029
+ else if (xhr.status >= 500) {
2030
+ userMessage = "Lỗi máy chủ. Vui lòng thử lại sau.";
2031
+ }
2032
+ else if (xhr.status >= 400) {
2033
+ userMessage = `Lỗi yêu cầu: ${xhr.statusText}`;
2034
+ }
2035
+ cleanup();
2036
+ observer.error(this.buildHttpError(userMessage, {
2037
+ status: xhr.status,
2038
+ statusText: xhr.statusText,
2039
+ url: presignedURL,
2040
+ }));
2041
+ };
2042
+ const timeoutHandler = () => {
2043
+ cleanup();
2044
+ observer.error(this.buildHttpError("Kết nối đến máy chủ hết thời gian. Vui lòng thử lại.", {
2045
+ status: xhr?.status,
2046
+ statusText: xhr?.statusText,
2047
+ url: presignedURL,
2048
+ }));
2049
+ };
2050
+ xhr.upload.addEventListener("progress", progressHandler);
2051
+ xhr.addEventListener("load", loadHandler);
2052
+ xhr.addEventListener("error", errorHandler);
2053
+ xhr.addEventListener("timeout", timeoutHandler);
2054
+ xhr.timeout = 60000 * 5;
2055
+ xhr.open("PUT", presignedURL, true);
2056
+ xhr.setRequestHeader("Content-Type", contentType);
2057
+ xhr.send(file);
2058
+ })
2059
+ .catch((error) => {
2060
+ if (xhr) {
2061
+ xhr.abort();
2062
+ xhr = null;
2063
+ }
2064
+ let userMessage = "Upload file thất bại";
2065
+ if (error.errorMessage) {
2066
+ userMessage = error.errorMessage;
2067
+ }
2068
+ else if (error.code === "ECONNABORTED" ||
2069
+ error.message?.includes("timeout")) {
2070
+ userMessage =
2071
+ "Kết nối đến máy chủ hết thời gian. Vui lòng thử lại.";
2072
+ }
2073
+ else if (error.message?.includes("failed to respond") ||
2074
+ error.message?.includes("Network Error")) {
2075
+ userMessage =
2076
+ "Không thể kết nối đến máy chủ lưu trữ. Máy chủ không phản hồi.";
2077
+ }
2078
+ else if (error.message) {
2079
+ userMessage = `Lỗi kết nối: ${error.message}`;
2080
+ }
2081
+ else if (error.error?.message) {
2082
+ userMessage = error.error.message;
2083
+ }
2084
+ observer.error(this.buildHttpError(userMessage, {
2085
+ status: error?.status,
2086
+ statusText: error?.statusText,
2087
+ url: error?.url,
2088
+ cause: error,
2089
+ }));
2090
+ });
2091
+ return () => {
2092
+ if (xhr) {
2093
+ xhr.abort();
2094
+ xhr = null;
2095
+ }
2096
+ };
2097
+ });
2098
+ }
2099
+ /**
2100
+ * Tạo tên file unique bằng cách thêm timestamp và random string
2101
+ * Format: originalname-timestamp-random.ext
2102
+ */
2103
+ generateUniqueFileName(originalFileName) {
2104
+ const timestamp = Date.now();
2105
+ const randomString = Math.random().toString(36).substring(2, 9);
2106
+ // Tách tên file và extension
2107
+ const lastDotIndex = originalFileName.lastIndexOf(".");
2108
+ if (lastDotIndex === -1) {
2109
+ // Không có extension
2110
+ return `${originalFileName}-${timestamp}-${randomString}`;
2111
+ }
2112
+ const nameWithoutExt = originalFileName.substring(0, lastDotIndex);
2113
+ const extension = originalFileName.substring(lastDotIndex);
2114
+ return `${nameWithoutExt}-${timestamp}-${randomString}${extension}`;
2115
+ }
2116
+ extractFileName(filePath) {
2117
+ return filePath.split("/").pop() || "";
2118
+ }
2119
+ buildResult(file, filePath, url) {
2120
+ return {
2121
+ url,
2122
+ filePath,
2123
+ fileName: this.extractFileName(filePath) || file.name,
2124
+ originalName: file.name,
2125
+ };
2126
+ }
1743
2127
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileAIA_GCSService, deps: [{ token: i1$2.HttpClient }, { token: ApiService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1744
2128
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileAIA_GCSService, providedIn: "root" }); }
1745
2129
  }
@@ -1784,7 +2168,7 @@ class FileWAPGCSService {
1784
2168
  formData.append("file", file);
1785
2169
  const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1786
2170
  reportProgress: true,
1787
- responseType: "json"
2171
+ responseType: "json",
1788
2172
  });
1789
2173
  return this._http.request(req);
1790
2174
  }
@@ -1793,17 +2177,17 @@ class FileWAPGCSService {
1793
2177
  formData.append("file", file);
1794
2178
  const req = new HttpRequest("POST", `${this.baseUrl}/attachFile`, formData, {
1795
2179
  reportProgress: true,
1796
- responseType: "json"
2180
+ responseType: "json",
1797
2181
  });
1798
2182
  return firstValueFrom(this._http.request(req));
1799
2183
  }
1800
2184
  uploadFile(file) {
1801
- return new Observable(observer => {
2185
+ return new Observable((observer) => {
1802
2186
  let xhr = null;
1803
2187
  // Tạo tên file unique để tránh trùng tên khi upload nhiều lần
1804
2188
  const uniqueFileName = this.generateUniqueFileName(file.name);
1805
2189
  firstValueFrom(this.getPresignedLink(uniqueFileName))
1806
- .then(presignedData => {
2190
+ .then((presignedData) => {
1807
2191
  if (!presignedData) {
1808
2192
  throw new Error("Không nhận được presigned link từ server");
1809
2193
  }
@@ -1815,7 +2199,10 @@ class FileWAPGCSService {
1815
2199
  if (!presignedURL) {
1816
2200
  throw new Error("Presigned URL không hợp lệ");
1817
2201
  }
1818
- const contentType = file.type || "application/octet-stream";
2202
+ const contentType = presignedData.ContentType ||
2203
+ presignedData.contentType ||
2204
+ file.type ||
2205
+ "application/octet-stream";
1819
2206
  xhr = new XMLHttpRequest();
1820
2207
  const cleanup = () => {
1821
2208
  if (xhr) {
@@ -1853,7 +2240,7 @@ class FileWAPGCSService {
1853
2240
  status: xhr?.status,
1854
2241
  statusText: xhr?.statusText,
1855
2242
  url: presignedURL,
1856
- cause: err
2243
+ cause: err,
1857
2244
  }));
1858
2245
  }
1859
2246
  }
@@ -1863,7 +2250,7 @@ class FileWAPGCSService {
1863
2250
  observer.error(this.buildHttpError(errorMessage, {
1864
2251
  status: xhr.status,
1865
2252
  statusText: xhr.statusText,
1866
- url: presignedURL
2253
+ url: presignedURL,
1867
2254
  }));
1868
2255
  }
1869
2256
  };
@@ -1885,7 +2272,7 @@ class FileWAPGCSService {
1885
2272
  observer.error(this.buildHttpError(userMessage, {
1886
2273
  status: xhr.status,
1887
2274
  statusText: xhr.statusText,
1888
- url: presignedURL
2275
+ url: presignedURL,
1889
2276
  }));
1890
2277
  };
1891
2278
  const timeoutHandler = () => {
@@ -1893,7 +2280,7 @@ class FileWAPGCSService {
1893
2280
  observer.error(this.buildHttpError("Kết nối đến máy chủ hết thời gian. Vui lòng thử lại.", {
1894
2281
  status: xhr?.status,
1895
2282
  statusText: xhr?.statusText,
1896
- url: presignedURL
2283
+ url: presignedURL,
1897
2284
  }));
1898
2285
  };
1899
2286
  xhr.upload.addEventListener("progress", progressHandler);
@@ -1905,7 +2292,7 @@ class FileWAPGCSService {
1905
2292
  xhr.setRequestHeader("Content-Type", contentType);
1906
2293
  xhr.send(file);
1907
2294
  })
1908
- .catch(error => {
2295
+ .catch((error) => {
1909
2296
  if (xhr) {
1910
2297
  xhr.abort();
1911
2298
  xhr = null;
@@ -1935,7 +2322,7 @@ class FileWAPGCSService {
1935
2322
  status: error?.status,
1936
2323
  statusText: error?.statusText,
1937
2324
  url: error?.url,
1938
- cause: error
2325
+ cause: error,
1939
2326
  }));
1940
2327
  });
1941
2328
  return () => {
@@ -1971,7 +2358,7 @@ class FileWAPGCSService {
1971
2358
  url,
1972
2359
  filePath,
1973
2360
  fileName: this.extractFileName(filePath) || file.name,
1974
- originalName: file.name
2361
+ originalName: file.name,
1975
2362
  };
1976
2363
  }
1977
2364
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileWAPGCSService, deps: [{ token: i1$2.HttpClient }, { token: ApiService }], target: i0.ɵɵFactoryTarget.Injectable }); }