66from urllib .parse import urljoin
77from helpers import Item , ItemType , StrapiBlog
88from tqdm .auto import tqdm
9- from datetime import datetime
9+ from datetime import datetime , timezone
1010from pathlib import Path
11+ from time import sleep
1112
1213args = None
1314
14- BASE_URL = os .getenv (' STRAPI_URL' , "" )
15- API_KEY = os .getenv (' STRAPI_API_KEY' , "" )
15+ BASE_URL = os .getenv (" STRAPI_URL" , "" )
16+ API_KEY = os .getenv (" STRAPI_API_KEY" , "" )
1617
1718paths_to_search = []
1819existing_filepaths_discovered = {}
1920
20- headers = {
21- 'Authorization' : f'Bearer { API_KEY } ' ,
22- 'Content-Type' : 'application/json'
23- }
21+ headers = {"Authorization" : f"Bearer { API_KEY } " , "Content-Type" : "application/json" }
22+
2423
2524def arg_parse ():
2625 global args
2726 parser = argparse .ArgumentParser (description = "VectorHub Strapi Upload" )
28- parser .add_argument ('--directories' , help = 'Path to json which describes the directories to parse' )
27+ parser .add_argument (
28+ "--directories" , help = "Path to json which describes the directories to parse"
29+ )
2930 args = parser .parse_args ()
3031
32+
3133def load_items_from_json (directories : str ) -> list :
3234 if os .path .exists (directories ):
33- items = []
3435 try :
35- with open (directories , 'r' ) as file :
36+ with open (directories , "r" ) as file :
3637 data = json .load (file )
37- for item_data in data :
38- items .append (Item .from_dict (item_data ))
39- except JSONDecodeError as e :
40- print ('JSON Structure is invalid.' )
38+ return [Item .from_dict (item_data ) for item_data in data ]
39+ except JSONDecodeError :
40+ print ("❌ Invalid JSON structure." )
4141 exit (1 )
4242 except Exception as e :
43- print (' Unknown error occured.' )
43+ print ("❌ Unknown error while reading directory JSON:" )
4444 print (e )
4545 exit (1 )
46- return items
4746 else :
4847 print (f"{ directories } does not exist." )
4948 exit (1 )
5049
5150
5251def load_existing_blogs (page_num = 1 ):
52+ """Loads all blogs currently in Strapi."""
5353 global existing_filepaths_discovered
54- base_url = urljoin (BASE_URL , 'api/blogs' )
55- search_url = base_url + f"?pagination[page]={ page_num } &publicationState=preview"
5654
57- session = requests .Session ()
55+ base_url = urljoin (BASE_URL , "api/blogs" )
56+ search_url = f"{ base_url } ?pagination[page]={ page_num } &pagination[pageSize]=100"
5857
58+ session = requests .Session ()
5959 response = session .get (search_url , headers = headers )
60+
6061 if response .status_code == 200 :
61- data = json .loads (response .text )['data' ]
62- if len (data ) > 0 :
63- for item in data :
64- existing_filepaths_discovered [item ['attributes' ]['filepath' ]] = {'discovered' : False , 'id' : item ['id' ]}
65- load_existing_blogs (page_num + 1 )
62+ data = response .json ().get ("data" , [])
63+ if not data :
64+ return
65+ for item in data :
66+ filepath = item .get ("filepath" )
67+ if filepath :
68+ existing_filepaths_discovered [filepath ] = {
69+ "discovered" : False ,
70+ "id" : item ["id" ],
71+ }
72+ load_existing_blogs (page_num + 1 )
73+ else :
74+ print (f"⚠️ Failed to load blogs: { response .status_code } { response .text } " )
6675
6776
6877def fetch_paths (node : Item , current_path = "" ):
78+ """Recursively collect directories containing blogs."""
6979 global paths_to_search
70- # Update the current path with dthe node's path
80+
7181 current_path = f"{ current_path } /{ node .path } " if current_path else node .path
7282
73- # If the node has children, recurse on each child
7483 if node .has_blogs :
7584 paths_to_search .append (current_path )
76- if node .children and len ( node . children ) > 0 :
85+ if node .children :
7786 for child in node .children :
7887 fetch_paths (child , current_path )
7988
@@ -85,78 +94,102 @@ def find_files_to_upload(items: list):
8594 fetch_paths (item )
8695
8796 files = []
97+ extension = "md"
8898
89- extension = 'md'
90-
9199 for path in paths_to_search :
92100 folder_path = Path (path )
93- folder_files = folder_path .glob (f"*.{ extension } " )
94- for file in folder_files :
95- if 'readme.md' not in str (file ).lower ():
96- files .append ({
97- 'path' : str (file ),
98- 'time' : datetime .fromtimestamp (os .path .getmtime (file )).strftime ("%Y-%m-%d" )
99- })
100-
101+ for file in folder_path .glob (f"*.{ extension } " ):
102+ if "readme.md" not in str (file ).lower ():
103+ files .append (
104+ {
105+ "path" : str (file ),
106+ "time" : datetime .fromtimestamp (os .path .getmtime (file )).strftime (
107+ "%Y-%m-%d"
108+ ),
109+ }
110+ )
101111 return files
102112
103113
104114def build_blog_object (file_obj : dict ) -> StrapiBlog :
105- filepath = file_obj [' path' ]
106- with open (filepath , 'r' ) as file :
115+ filepath = file_obj [" path" ]
116+ with open (filepath , "r" ) as file :
107117 content = file .read ()
108- blog = StrapiBlog (content , filepath , file_obj [' time' ])
109- return blog
118+ return StrapiBlog (content , filepath , file_obj [" time" ])
119+
110120
111121def upload_blog (blog : StrapiBlog ):
112- base_url = urljoin (BASE_URL , 'api/blogs' )
122+ """Uploads or updates a blog to Strapi v5."""
123+ base_url = urljoin (BASE_URL , "api/blogs" )
113124 filepath = blog .get_filepath ()
114- search_url = base_url + f"?filters[filepath][$eqi]={ filepath } &publicationState=preview"
125+ search_url = f"{ base_url } ?filters[filepath][$eqi]={ filepath } "
126+
115127 session = requests .Session ()
116128
117129 if filepath in existing_filepaths_discovered :
118- existing_filepaths_discovered [filepath ][' discovered' ] = True
130+ existing_filepaths_discovered [filepath ][" discovered" ] = True
119131
120132 response = session .get (search_url , headers = headers )
133+ if response .status_code != 200 :
134+ print (f"❌ Error fetching blog { filepath } : { response .text } " )
135+ return
121136
122- if response .status_code == 200 :
123- responses = json .loads (response .text )['data' ]
124- print (f'Uploading filepath: { blog .get_filepath ()} ' )
125- if len (responses ) > 0 :
126- # Blog already exists at this filepath
127- id = json .loads (response .text )['data' ][0 ]['id' ]
137+ existing = response .json ().get ("data" , [])
138+ print (f"📤 Uploading filepath: { filepath } " )
128139
129- blog .set_slug_url (json .loads (response .text )['data' ][0 ]['attributes' ]['slug_url' ])
130- blog .set_published_at (json .loads (response .text )['data' ][0 ]['attributes' ]['publishedAt' ])
140+ if existing :
141+ blog_id = existing [0 ]["documentId" ]
142+ blog .set_slug_url (existing [0 ].get ("slug_url" ))
143+ blog .set_published_at (existing [0 ].get ("published_date" ))
131144
132- url = f"{ base_url } /{ id } "
133- create_response = session .put (url , headers = headers , data = json .dumps (blog .get_post_json ()))
145+ meta_desc = existing [0 ].get ("meta_desc" )
146+ if meta_desc :
147+ blog .meta_desc = meta_desc
134148 else :
135- # Its a new blog
136- url = base_url
137- create_response = session .post (url , headers = headers , data = json .dumps (blog .get_post_json ()))
149+ blog .meta_desc = blog .title
150+
151+ print ("Updating Blog" , blog )
152+
153+ url = f"{ base_url } /{ blog_id } "
154+ create_response = session .put (
155+ url , headers = headers , data = json .dumps (blog .get_post_json ())
156+ )
157+ else :
158+ # New blog
159+ blog .meta_desc = blog .title
160+ blog .set_published_at (
161+ datetime .now (timezone .utc )
162+ .isoformat (timespec = "milliseconds" )
163+ .replace ("+00:00" , "Z" )
164+ )
165+ print ("Adding Blog" , blog )
166+ create_response = session .post (
167+ f"{ base_url } ?status=draft" ,
168+ headers = headers ,
169+ data = json .dumps (blog .get_post_json ()),
170+ )
171+
172+ if create_response .status_code not in (200 , 201 ):
173+ print (f"❌ Failed to upload blog: { filepath } " , create_response .text )
174+ exit (1 )
138175
139- if not create_response .status_code == 200 :
140- print (f'Error in parsing blog: { filepath } ' )
141- print (create_response .text )
142- exit (1 )
143176
144177def delete_old_blogs ():
145- global existing_filepaths_discovered , BASE_URL
178+ """Deletes blogs that were not re-uploaded."""
179+ global existing_filepaths_discovered
146180
147- base_url = urljoin (BASE_URL , ' api/blogs' )
181+ base_url = urljoin (BASE_URL , " api/blogs" )
148182 session = requests .Session ()
149183
150- for filepath in existing_filepaths_discovered :
151- if not existing_filepaths_discovered [ filepath ][ ' discovered' ]:
152- print (f"Deleting filepath: { filepath } " )
153- id = existing_filepaths_discovered [ filepath ][ 'id' ]
154- if id > 0 :
155- url = f"{ base_url } /{ id } "
184+ for filepath , info in existing_filepaths_discovered . items () :
185+ if not info [ " discovered" ]:
186+ print (f"🗑️ Deleting filepath: { filepath } " )
187+ blog_id = info [ "id" ]
188+ if blog_id :
189+ url = f"{ base_url } /{ blog_id } "
156190 response = session .delete (url , headers = headers )
157- if response .status_code != 200 :
158- print (f'Error in deleting blog: { filepath } ' )
159- print (response .text )
191+ if response .status_code not in (200 , 204 ):
192+ print (f"⚠️ Error deleting { filepath } : { response .text } " )
160193
161194
162195if __name__ == "__main__" :
@@ -167,10 +200,11 @@ def delete_old_blogs():
167200
168201 files = find_files_to_upload (items )
169202
170- print (' Uploading blogs' )
203+ print ("📦 Uploading blogs..." )
171204 for file in tqdm (files ):
205+ sleep (0.7 ) # To avoid overwhelming the server
172206 blog = build_blog_object (file )
173207 upload_blog (blog )
174208
175- print ('Deleting blogs' )
209+ print ("🧹 Cleaning up deleted blogs..." )
176210 delete_old_blogs ()
0 commit comments