@@ -231,25 +231,44 @@ class Attachment(Document):
231231
232232 @property
233233 def extension (self ) -> str :
234+ # Prefer explicit draw.io markers
234235 if self .comment == "draw.io diagram" and self .media_type == "application/vnd.jgraph.mxfile" :
235236 return ".drawio"
236237 if self .comment == "draw.io preview" and self .media_type == "image/png" :
237238 return ".drawio.png"
238239
239- return mimetypes .guess_extension (self .media_type ) or ""
240+ # First try to guess from media_type
241+ guessed = mimetypes .guess_extension (self .media_type ) or ""
242+ if guessed :
243+ return guessed
244+
245+ # Fallback to using the title's suffix (if present)
246+ try :
247+ title_suffix = Path (self .title ).suffix
248+ except Exception :
249+ title_suffix = ""
250+
251+ return title_suffix or ""
240252
241253 @property
242254 def filename (self ) -> str :
243- return f"{ self .file_id } { self .extension } "
255+ # If file_id is available use it, otherwise fall back to a sanitized title stem
256+ if self .file_id :
257+ return f"{ self .file_id } { self .extension } "
258+ # Use the title stem (filename without extension), sanitized for filesystem
259+ stem = sanitize_filename (Path (self .title ).stem )
260+ return f"{ stem } { self .extension } "
244261
245262 @property
246263 def _template_vars (self ) -> dict [str , str ]:
247264 return {
248265 ** super ()._template_vars ,
249266 "attachment_id" : str (self .id ),
250267 "attachment_title" : sanitize_filename (self .title ),
251- # file_id is a GUID and does not need sanitized.
252- "attachment_file_id" : self .file_id ,
268+ # file_id is a GUID and does not need sanitized. When missing, fall back to
269+ # a sanitized title stem so templates that only reference {attachment_file_id}
270+ # don't end up with filenames like ".png".
271+ "attachment_file_id" : self .file_id or sanitize_filename (Path (self .title ).stem ),
253272 "attachment_extension" : self .extension ,
254273 }
255274
@@ -317,8 +336,25 @@ def export(self) -> None:
317336 logger .warning (f"There is no attachment with title '{ self .title } '. Skipping export." )
318337 return
319338
339+ # If the configured template produced a filepath without an extension
340+ # but the response content is JSON (or the content-type indicates JSON),
341+ # save the file with a .json suffix so consumers can recognise it.
342+ content_type = (response .headers .get ("Content-Type" ) or "" ).lower ()
343+ target_filepath = filepath
344+ looks_like_json = False
345+ try :
346+ # Quick binary check for JSON-like content
347+ body_start = response .content .lstrip ()[:1 ]
348+ if body_start in (b"{" , b"[" ):
349+ looks_like_json = True
350+ except Exception :
351+ looks_like_json = False
352+
353+ if not filepath .suffix and ("json" in content_type or looks_like_json ):
354+ target_filepath = filepath .with_suffix (".json" )
355+
320356 save_file (
321- filepath ,
357+ target_filepath ,
322358 response .content ,
323359 )
324360
@@ -782,7 +818,7 @@ def convert_jira_issue(self, el: BeautifulSoup, text: str, parent_tags: list[str
782818 try :
783819 issue = JiraIssue .from_key (str (issue_key ))
784820 return f"[[{ issue .key } ] { issue .summary } ]({ link .get ('href' )} )"
785- except HTTPError :
821+ except ( HTTPError , AttributeError ) :
786822 return f"[[{ issue_key } ]]({ link .get ('href' )} )"
787823
788824 def convert_pre (self , el : BeautifulSoup , text : str , parent_tags : list [str ]) -> str :
0 commit comments