2222 * This class writes JSON directly to a file, entry by entry, without keeping everything in memory.
2323 */
2424class JsonFileFeed implements FeedInterface {
25+ public const UPLOAD_DIR = 'product-feeds ' ;
26+
2527 /**
2628 * Indicates if there are previous entries in the feed.
2729 *
@@ -30,25 +32,32 @@ class JsonFileFeed implements FeedInterface {
3032 private $ has_entries = false ;
3133
3234 /**
33- * The path to the feed file.
35+ * The base name of the feed file.
3436 *
3537 * @var string
3638 */
37- private $ file_path ;
39+ private $ base_name ;
3840
3941 /**
40- * The file handle .
42+ * The name of the feed file, no directory .
4143 *
42- * @var resource|null
44+ * @var string
4345 */
44- private $ file_handle = null ;
46+ private $ file_name ;
4547
4648 /**
47- * The base name of the feed file.
49+ * The path to the feed file.
4850 *
4951 * @var string
5052 */
51- private $ base_name ;
53+ private $ file_path ;
54+
55+ /**
56+ * The file handle.
57+ *
58+ * @var resource|null
59+ */
60+ private $ file_handle = null ;
5261
5362 /**
5463 * Indicates if the feed file has been completed.
@@ -64,6 +73,13 @@ class JsonFileFeed implements FeedInterface {
6473 */
6574 private $ file_url = null ;
6675
76+ /**
77+ * Indicates if the feed file is in a temp directory.
78+ *
79+ * @var bool
80+ */
81+ private $ is_temp_filepath = false ;
82+
6783 /**
6884 * Constructor.
6985 *
@@ -80,27 +96,6 @@ public function __construct( string $base_name ) {
8096 * @throws Exception If the feed directory cannot be created.
8197 */
8298 public function start (): void {
83- $ upload_dir = wp_upload_dir ( null , true );
84- $ directory = $ upload_dir ['basedir ' ] . DIRECTORY_SEPARATOR . 'product-feeds ' . DIRECTORY_SEPARATOR ;
85-
86- // Try to create the directory if it does not exist.
87- if ( ! is_dir ( $ directory ) ) {
88- FileSystemUtil::mkdir_p_not_indexable ( $ directory );
89- }
90-
91- // `mkdir_p_not_indexable()` returns `void`, so we need to check again.
92- if ( ! is_dir ( $ directory ) ) {
93- throw new Exception (
94- esc_html (
95- sprintf (
96- /* translators: %s: directory path */
97- __ ( 'Unable to create feed directory: %s ' , 'woocommerce-product-feed-openai ' ),
98- $ directory
99- )
100- )
101- );
102- }
103-
10499 /**
105100 * Allows the current time to be overridden before a feed is stored.
106101 *
@@ -109,19 +104,26 @@ public function start(): void {
109104 * @return int The current time.
110105 * @since 0.1.0
111106 */
112- $ current_time = apply_filters ( 'wpfoai_feed_time ' , time (), $ this );
113- $ hash_data = $ this ->base_name . gmdate ( 'r ' , $ current_time );
114- $ file_name = sprintf (
107+ $ current_time = apply_filters ( 'wpfoai_feed_time ' , time (), $ this );
108+ $ hash_data = $ this ->base_name . gmdate ( 'r ' , $ current_time );
109+ $ this -> file_name = sprintf (
115110 '%s-%s-%s.json ' ,
116111 $ this ->base_name ,
117112 gmdate ( 'Y-m-d ' , $ current_time ),
118113 wp_hash ( $ hash_data )
119114 );
120115
121- $ this ->file_path = $ directory . $ file_name ;
122- $ this ->file_url = $ upload_dir ['baseurl ' ] . '/product-feeds/ ' . $ file_name ;
123-
116+ // Start by trying to use a temp directory to generate the feed.
117+ $ this ->file_path = get_temp_dir () . DIRECTORY_SEPARATOR . $ this ->file_name ;
124118 $ this ->file_handle = fopen ( $ this ->file_path , 'w ' );
119+ if ( false === $ this ->file_handle ) {
120+ // Fall back to immediately using the upload directory for generation.
121+ $ upload_dir = $ this ->get_upload_dir ();
122+ $ this ->file_path = $ upload_dir ['path ' ] . $ this ->file_name ;
123+ $ this ->file_handle = fopen ( $ this ->file_path , 'w ' );
124+ } else {
125+ $ this ->is_temp_filepath = true ;
126+ }
125127
126128 if ( false === $ this ->file_handle ) {
127129 throw new Exception (
@@ -170,24 +172,104 @@ public function end(): void {
170172 }
171173
172174 /**
173- * Get the path to the feed file.
174- *
175- * @return string The path to the feed file.
175+ * {@inheritDoc}
176176 */
177- public function get_file_path (): string {
177+ public function get_file_path (): ?string {
178+ if ( ! $ this ->file_completed ) {
179+ return null ;
180+ }
181+
178182 return $ this ->file_path ;
179183 }
180184
181185 /**
182- * Get the URL of the feed file.
186+ * {@inheritDoc}
183187 *
184- * @return string|null The URL of the feed file, null if not completed .
188+ * @throws Exception If the feed file cannot be moved to the upload directory .
185189 */
186190 public function get_file_url (): ?string {
187191 if ( ! $ this ->file_completed ) {
188192 return null ;
189193 }
190194
195+ $ upload_dir = $ this ->get_upload_dir ();
196+
197+ // Move the file to the upload directory if it is in temp.
198+ if ( $ this ->is_temp_filepath ) {
199+ $ tmp_path = $ this ->file_path ;
200+ $ this ->file_path = $ upload_dir ['path ' ] . $ this ->file_name ;
201+ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
202+ if ( ! @copy ( $ tmp_path , $ this ->file_path ) ) {
203+ $ error = error_get_last ();
204+ throw new Exception (
205+ esc_html (
206+ sprintf (
207+ /* translators: %1$s: file path, %2$s: error message */
208+ __ ( 'Unable to move feed file %1$s to upload directory: %2$s ' , 'woocommerce-product-feed-openai ' ),
209+ $ this ->file_path ,
210+ ( esc_html ( $ error ['message ' ] ) ?? 'Unknown error ' )
211+ )
212+ )
213+ );
214+ }
215+
216+ unlink ( $ tmp_path );
217+
218+ $ this ->is_temp_filepath = false ;
219+ }
220+
221+ // Generate the URL.
222+ $ this ->file_url = $ upload_dir ['url ' ] . $ this ->file_name ;
223+
191224 return $ this ->file_url ;
192225 }
226+
227+ /**
228+ * Get the upload directory for the feed.
229+ *
230+ * @return array {
231+ * The upload directory for the feed. Both fields end with the right trailing slash.
232+ *
233+ * @type string $path The path to the upload directory.
234+ * @type string $url The URL to the upload directory.
235+ * }
236+ * @throws Exception If the upload directory cannot be created.
237+ */
238+ private function get_upload_dir (): array {
239+ // Only generate everything once.
240+ static $ prepared ;
241+ if ( isset ( $ prepared ) ) {
242+ return $ prepared ;
243+ }
244+
245+ $ upload_dir = wp_upload_dir ( null , true );
246+ $ directory_path = $ upload_dir ['basedir ' ] . DIRECTORY_SEPARATOR . self ::UPLOAD_DIR . DIRECTORY_SEPARATOR ;
247+
248+ // Try to create the directory if it does not exist.
249+ if ( ! is_dir ( $ directory_path ) ) {
250+ FilesystemUtil::mkdir_p_not_indexable ( $ directory_path );
251+ }
252+
253+ // `mkdir_p_not_indexable()` returns `void`, we have to check again.
254+ if ( ! is_dir ( $ directory_path ) ) {
255+ throw new Exception (
256+ esc_html (
257+ sprintf (
258+ /* translators: %s: directory path */
259+ __ ( 'Unable to create feed directory: %s ' , 'woocommerce-product-feed-openai ' ),
260+ $ directory_path
261+ )
262+ )
263+ );
264+ }
265+
266+ $ directory_url = $ upload_dir ['baseurl ' ] . '/ ' . self ::UPLOAD_DIR . '/ ' ;
267+
268+ // Follow the format, returned by `wp_upload_dir()`.
269+ $ prepared = [
270+ 'path ' => $ directory_path ,
271+ 'url ' => $ directory_url ,
272+ ];
273+ return $ prepared ;
274+ }
193275}
0 commit comments