77use Utopia \Database \Exception \Conflict ;
88use Utopia \Database \Exception \Structure ;
99use Utopia \Migration \Destination ;
10- use Utopia \Migration \Resource ;
10+ use Utopia \Migration \Resource as UtopiaResource ;
1111use Utopia \Migration \Resources \Database \Row ;
1212use Utopia \Migration \Transfer ;
1313use Utopia \Storage \Device ;
@@ -37,7 +37,7 @@ public function __construct(
3737 array $ allowedColumns = [],
3838 private readonly string $ delimiter = ', ' ,
3939 private readonly string $ enclosure = '" ' ,
40- private readonly string $ escape = '\\ ' ,
40+ private readonly string $ escape = '" ' ,
4141 private readonly bool $ includeHeaders = true ,
4242 ) {
4343 $ this ->deviceForFiles = $ deviceForFiles ;
@@ -61,7 +61,7 @@ public static function getName(): string
6161 public static function getSupportedResources (): array
6262 {
6363 return [
64- Resource ::TYPE_ROW ,
64+ UtopiaResource ::TYPE_ROW ,
6565 ];
6666 }
6767
@@ -95,7 +95,7 @@ protected function import(array $resources, callable $callback): void
9595 }
9696
9797 foreach ($ buffer ['lines ' ] as $ line ) {
98- if (\fputcsv ( $ handle , $ line , $ this ->delimiter , $ this -> enclosure , $ this -> escape ) === false ) {
98+ if (! $ this ->writeCSVLine ( $ handle , $ line ) ) {
9999 throw new \Exception ("Failed to write CSV line to file: $ log " );
100100 }
101101 }
@@ -138,7 +138,7 @@ protected function import(array $resources, callable $callback): void
138138 $ flushBuffer ();
139139 }
140140
141- $ resource ->setStatus (Resource ::STATUS_SUCCESS );
141+ $ resource ->setStatus (UtopiaResource ::STATUS_SUCCESS );
142142 if (isset ($ this ->cache )) {
143143 $ this ->cache ->update ($ resource );
144144 }
@@ -166,13 +166,11 @@ public function shutdown(): void
166166 $ sourcePath = $ this ->local ->getPath ($ filename );
167167 $ destPath = $ this ->deviceForFiles ->getPath ($ this ->directory . '/ ' . $ filename );
168168
169- // Check if the CSV file was actually created
170169 if (!$ this ->local ->exists ($ sourcePath )) {
171170 throw new \Exception ("No data to export for resource: $ this ->resourceId " );
172171 }
173172
174173 try {
175- // Transfer expects absolute paths within each device
176174 $ result = $ this ->local ->transfer (
177175 $ sourcePath ,
178176 $ destPath ,
@@ -192,6 +190,29 @@ public function shutdown(): void
192190 }
193191 }
194192
193+ /**
194+ * Write a CSV line with RFC 4180 compliant escaping (double-quote method)
195+ *
196+ * @param resource $handle
197+ * @param array $fields
198+ * @return bool
199+ */
200+ protected function writeCSVLine ($ handle , array $ fields ): bool
201+ {
202+ $ parts = [];
203+
204+ foreach ($ fields as $ field ) {
205+ $ field = (string )$ field ;
206+ if (\strpbrk ($ field , $ this ->delimiter . "\n\r" . $ this ->enclosure ) !== false ) {
207+ $ parts [] = $ this ->enclosure . \str_replace ($ this ->enclosure , $ this ->enclosure . $ this ->enclosure , $ field ) . $ this ->enclosure ;
208+ } else {
209+ $ parts [] = $ field ;
210+ }
211+ }
212+
213+ return \fwrite ($ handle , \implode ($ this ->delimiter , $ parts ) . "\n" ) !== false ;
214+ }
215+
195216 /**
196217 * Helper to ensure a directory exists.
197218 * @throws \Exception
@@ -222,18 +243,25 @@ protected function sanitizeFilename(string $filename): string
222243 */
223244 protected function resourceToCSVData (Row $ resource ): array
224245 {
246+ $ rowData = $ resource ->getData ();
247+
225248 $ data = [
226249 '$id ' => $ resource ->getId (),
227250 '$permissions ' => $ resource ->getPermissions (),
228- '$createdAt ' => $ resource -> getCreatedAt () ,
229- '$updatedAt ' => $ resource -> getUpdatedAt () ,
251+ '$createdAt ' => $ rowData [ ' $createdAt ' ] ?? '' ,
252+ '$updatedAt ' => $ rowData [ ' $updatedAt ' ] ?? '' ,
230253 ];
231254
255+ unset(
256+ $ rowData ['$createdAt ' ],
257+ $ rowData ['$updatedAt ' ],
258+ );
259+
232260 // Add all attributes if no filter specified, otherwise only allowed ones
233261 if (empty ($ this ->allowedColumns )) {
234- $ data = \array_merge ($ data , $ resource -> getData () );
262+ $ data = \array_merge ($ data , $ rowData );
235263 } else {
236- foreach ($ resource -> getData () as $ key => $ value ) {
264+ foreach ($ rowData as $ key => $ value ) {
237265 if (isset ($ this ->allowedColumns [$ key ])) {
238266 $ data [$ key ] = $ value ;
239267 }
@@ -291,5 +319,4 @@ protected function convertObjectToCSV($value): string
291319 }
292320 return \json_encode ($ value );
293321 }
294-
295322}
0 commit comments