From: Adam Dullage Date: Fri, 29 Jul 2022 11:39:14 +0000 (+0100) Subject: Remove file extension from Python and Web APIs. Resolves #17. X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=c4a3337fbb7f741c283d7c008d9d1e562739c8be;p=flatnotes.git Remove file extension from Python and Web APIs. Resolves #17. --- diff --git a/flatnotes/error_responses.py b/flatnotes/error_responses.py index bcf452d..b5c4314 100644 --- a/flatnotes/error_responses.py +++ b/flatnotes/error_responses.py @@ -1,17 +1,15 @@ from fastapi.responses import JSONResponse -file_exists_response = JSONResponse( - content={"message": "The specified filename already exists."}, +title_exists_response = JSONResponse( + content={"message": "The specified title already exists."}, status_code=409, ) -invalid_filename_response = JSONResponse( - content={ - "message": "The specified filename contains invalid characters." - }, +invalid_title_response = JSONResponse( + content={"message": "The specified title contains invalid characters."}, status_code=400, ) -file_not_found_response = JSONResponse( - content={"message": "The specified file cannot be found."}, status_code=404 +note_not_found_response = JSONResponse( + content={"message": "The specified note cannot be found."}, status_code=404 ) diff --git a/flatnotes/flatnotes.py b/flatnotes/flatnotes.py index 9e8ca21..e0db69c 100644 --- a/flatnotes/flatnotes.py +++ b/flatnotes/flatnotes.py @@ -11,9 +11,13 @@ from whoosh.index import Index from whoosh.qparser import MultifieldParser from whoosh.searching import Hit +from helpers import strip_ext -class InvalidFilenameError(Exception): - def __init__(self, message="The specified filename is invalid"): +MARKDOWN_EXT = ".md" + + +class InvalidTitleError(Exception): + def __init__(self, message="The specified title is invalid"): self.message = message super().__init__(self.message) @@ -27,12 +31,12 @@ class IndexSchema(SchemaClass): class Note: def __init__( - self, flatnotes: "Flatnotes", filename: str, new: bool = False + self, flatnotes: "Flatnotes", title: str, new: bool = False ) -> None: - if not self._is_valid_filename(filename): - raise InvalidFilenameError self._flatnotes = flatnotes - self._filename = filename + self._title = title.strip() + if not self._is_valid_title(self._title): + raise InvalidTitleError if new and os.path.exists(self.filepath): raise FileExistsError elif new: @@ -40,11 +44,11 @@ class Note: @property def filepath(self): - return os.path.join(self._flatnotes.dir, self._filename) + return os.path.join(self._flatnotes.dir, self.filename) @property - def title(self): - return os.path.splitext(self._filename)[0] + def filename(self): + return self._title + MARKDOWN_EXT @property def last_modified(self): @@ -52,16 +56,18 @@ class Note: # Editable Properties @property - def filename(self): - return self._filename - - @filename.setter - def filename(self, new_filename): - if not self._is_valid_filename(new_filename): - raise InvalidFilenameError - new_filepath = os.path.join(self._flatnotes.dir, new_filename) + def title(self): + return self._title + + @title.setter + def title(self, new_title): + if not self._is_valid_title(new_title): + raise InvalidTitleError + new_filepath = os.path.join( + self._flatnotes.dir, new_title + MARKDOWN_EXT + ) os.rename(self.filepath, new_filepath) - self._filename = new_filename + self._title = new_title @property def content(self): @@ -79,18 +85,16 @@ class Note: os.remove(self.filepath) # Functions - def _is_valid_filename(self, filename: str) -> bool: - r"""Return False if the declared filename contains any of the following + def _is_valid_title(self, title: str) -> bool: + r"""Return False if the declared title contains any of the following characters: <>:"/\|?*""" invalid_chars = r'<>:"/\|?*' - return not any( - invalid_char in filename for invalid_char in invalid_chars - ) + return not any(invalid_char in title for invalid_char in invalid_chars) class NoteHit(Note): def __init__(self, flatnotes: "Flatnotes", hit: Hit) -> None: - super().__init__(flatnotes, hit["filename"]) + super().__init__(flatnotes, strip_ext(hit["filename"])) self.title_highlights = hit.highlights("title", text=self.title) self.content_highlights = hit.highlights( "content", @@ -127,7 +131,8 @@ class Flatnotes(object): self, writer: writing.IndexWriter, note: Note ) -> None: """Add a Note object to the index using the given writer. If the - filepath already exists in the index an update will be performed instead.""" + filename already exists in the index an update will be performed + instead.""" writer.update_document( filename=note.filename, last_modified=note.last_modified, @@ -136,10 +141,13 @@ class Flatnotes(object): ) def get_notes(self) -> List[Note]: - """Return a list containing a Note object for every file in the notes directory.""" + """Return a list containing a Note object for every file in the notes + directory.""" return [ - Note(self, os.path.split(filepath)[1]) - for filepath in glob.glob(os.path.join(self.dir, "*.md")) + Note(self, strip_ext(os.path.split(filepath)[1])) + for filepath in glob.glob( + os.path.join(self.dir, "*" + MARKDOWN_EXT) + ) ] def update_index(self, clean: bool = False) -> None: @@ -162,7 +170,9 @@ class Flatnotes(object): os.path.getmtime(idx_filepath) != idx_note["last_modified"] ): logging.info(f"'{idx_filename}' updated") - self._add_note_to_index(writer, Note(self, idx_filename)) + self._add_note_to_index( + writer, Note(self, strip_ext(idx_filename)) + ) indexed.add(idx_filename) # Ignore already indexed else: @@ -176,7 +186,8 @@ class Flatnotes(object): self.last_index_update = datetime.now() def update_index_debounced(self, clean: bool = False) -> None: - """Run update_index() but only if it hasn't been run in the last 10 seconds.""" + """Run update_index() but only if it hasn't been run in the last 10 + seconds.""" if ( self.last_index_update is None or (datetime.now() - self.last_index_update).seconds > 10 diff --git a/flatnotes/helpers.py b/flatnotes/helpers.py index 28b6fbf..577b25f 100644 --- a/flatnotes/helpers.py +++ b/flatnotes/helpers.py @@ -1,6 +1,12 @@ +import os + from pydantic import BaseModel +def strip_ext(filename): + return os.path.splitext(filename)[0] + + def camel_case(snake_case_str: str) -> str: """Return the declared snake_case string in camelCase.""" parts = [part for part in snake_case_str.split("_") if part != ""] diff --git a/flatnotes/main.py b/flatnotes/main.py index c1b627c..f04fbeb 100644 --- a/flatnotes/main.py +++ b/flatnotes/main.py @@ -9,16 +9,16 @@ from auth import ( validate_token, ) from error_responses import ( - file_exists_response, - file_not_found_response, - invalid_filename_response, + title_exists_response, + note_not_found_response, + invalid_title_response, ) from fastapi import Depends, FastAPI, HTTPException from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from models import LoginModel, NoteHitModel, NoteModel, NotePatchModel -from flatnotes import Flatnotes, InvalidFilenameError, Note +from flatnotes import Flatnotes, InvalidTitleError, Note logging.basicConfig( format="%(asctime)s [%(levelname)s]: %(message)s", @@ -47,8 +47,8 @@ async def token(data: LoginModel): @app.get("/") @app.get("/login") @app.get("/search") -@app.get("/note/{filename}") -async def root(filename: str = ""): +@app.get("/note/{title}") +async def root(title: str = ""): with open("flatnotes/dist/index.html", "r", encoding="utf-8") as f: html = f.read() return HTMLResponse(content=html) @@ -58,7 +58,7 @@ async def root(filename: str = ""): async def get_notes( start: int = 0, limit: int = None, - sort: Literal["filename", "lastModified"] = "filename", + sort: Literal["title", "lastModified"] = "title", order: Literal["asc", "desc"] = "asc", include_content: bool = False, _: str = Depends(validate_token), @@ -68,7 +68,7 @@ async def get_notes( notes.sort( key=lambda note: note.last_modified if sort == "lastModified" - else note.filename, + else note.title, reverse=order == "desc", ) return [ @@ -81,59 +81,59 @@ async def get_notes( async def post_note(data: NoteModel, _: str = Depends(validate_token)): """Create a new note.""" try: - note = Note(flatnotes, data.filename, new=True) + note = Note(flatnotes, data.title, new=True) note.content = data.content return NoteModel.dump(note, include_content=True) - except InvalidFilenameError: - return invalid_filename_response + except InvalidTitleError: + return invalid_title_response except FileExistsError: - return file_exists_response + return title_exists_response -@app.get("/api/notes/{filename}", response_model=NoteModel) +@app.get("/api/notes/{title}", response_model=NoteModel) async def get_note( - filename: str, + title: str, include_content: bool = True, _: str = Depends(validate_token), ): """Get a specific note.""" try: - note = Note(flatnotes, filename) + note = Note(flatnotes, title) return NoteModel.dump(note, include_content=include_content) - except InvalidFilenameError: - return invalid_filename_response + except InvalidTitleError: + return invalid_title_response except FileNotFoundError: - return file_not_found_response + return note_not_found_response -@app.patch("/api/notes/{filename}", response_model=NoteModel) +@app.patch("/api/notes/{title}", response_model=NoteModel) async def patch_note( - filename: str, new_data: NotePatchModel, _: str = Depends(validate_token) + title: str, new_data: NotePatchModel, _: str = Depends(validate_token) ): try: - note = Note(flatnotes, filename) - if new_data.new_filename is not None: - note.filename = new_data.new_filename + note = Note(flatnotes, title) + if new_data.new_title is not None: + note.title = new_data.new_title if new_data.new_content is not None: note.content = new_data.new_content return NoteModel.dump(note, include_content=True) - except InvalidFilenameError: - return invalid_filename_response + except InvalidTitleError: + return invalid_title_response except FileExistsError: - return file_exists_response + return title_exists_response except FileNotFoundError: - return file_not_found_response + return note_not_found_response -@app.delete("/api/notes/{filename}") -async def delete_note(filename: str, _: str = Depends(validate_token)): +@app.delete("/api/notes/{title}") +async def delete_note(title: str, _: str = Depends(validate_token)): try: - note = Note(flatnotes, filename) + note = Note(flatnotes, title) note.delete() - except InvalidFilenameError: - return invalid_filename_response + except InvalidTitleError: + return invalid_title_response except FileNotFoundError: - return file_not_found_response + return note_not_found_response @app.get("/api/search", response_model=List[NoteHitModel]) diff --git a/flatnotes/models.py b/flatnotes/models.py index 969dd9b..e2988be 100644 --- a/flatnotes/models.py +++ b/flatnotes/models.py @@ -11,26 +11,26 @@ class LoginModel(CamelCaseBaseModel): class NoteModel(CamelCaseBaseModel): - filename: str + title: str last_modified: Optional[int] content: Optional[str] @classmethod def dump(cls, note: Note, include_content: bool = True) -> Dict: return { - "filename": note.filename, + "title": note.title, "lastModified": note.last_modified, "content": note.content if include_content else None, } class NotePatchModel(CamelCaseBaseModel): - new_filename: Optional[str] + new_title: Optional[str] new_content: Optional[str] class NoteHitModel(CamelCaseBaseModel): - filename: str + title: str last_modified: int title_highlights: Optional[str] content_highlights: Optional[str] @@ -38,7 +38,7 @@ class NoteHitModel(CamelCaseBaseModel): @classmethod def dump(self, note_hit: NoteHit) -> Dict: return { - "filename": note_hit.filename, + "title": note_hit.title, "lastModified": note_hit.last_modified, "titleHighlights": note_hit.title_highlights, "contentHighlights": note_hit.content_highlights, diff --git a/flatnotes/src/classes.js b/flatnotes/src/classes.js index 29b0604..7a58597 100644 --- a/flatnotes/src/classes.js +++ b/flatnotes/src/classes.js @@ -1,32 +1,20 @@ import * as constants from "./constants"; class Note { - constructor(filename, lastModified, content) { - this.filename = filename; + constructor(title, lastModified, content) { + this.title = title; this.lastModified = lastModified; this.content = content; } - get title() { - if (this.filename) { - return this.filename.substring(0, this.filename.lastIndexOf(".")); - } else { - return null; - } - } - - get ext() { - return this.filename.substring(this.filename.lastIndexOf(".") + 1); - } - get href() { return `/${constants.basePaths.note}/${this.title}`; } } class SearchResult extends Note { - constructor(filename, lastModified, titleHighlights, contentHighlights) { - super(filename, lastModified); + constructor(title, lastModified, titleHighlights, contentHighlights) { + super(title, lastModified); this.titleHighlights = titleHighlights; this.contentHighlights = contentHighlights; } diff --git a/flatnotes/src/components/App.js b/flatnotes/src/components/App.js index dd95552..028ba30 100644 --- a/flatnotes/src/components/App.js +++ b/flatnotes/src/components/App.js @@ -145,7 +145,7 @@ export default { response.data.forEach(function(result) { parent.searchResults.push( new SearchResult( - result.filename, + result.title, result.lastModified, result.titleHighlights, result.contentHighlights @@ -162,25 +162,25 @@ export default { }, getContentForEditor: function() { - let draftContent = localStorage.getItem(this.currentNote.filename); + let draftContent = localStorage.getItem(this.currentNote.title); if (draftContent) { if (confirm("Do you want to resume the saved draft?")) { return draftContent; } else { - localStorage.removeItem(this.currentNote.filename); + localStorage.removeItem(this.currentNote.title); } } return this.currentNote.content; }, - loadNote: function(filename) { + loadNote: function(title) { let parent = this; this.noteLoadFailed = false; api - .get(`/api/notes/${filename}.${constants.markdownExt}`) + .get(`/api/notes/${title}`) .then(function(response) { parent.currentNote = new Note( - response.data.filename, + response.data.title, response.data.lastModified, response.data.content ); @@ -208,7 +208,7 @@ export default { // To Edit Mode if (this.editMode == false) { this.titleInput = this.currentNote.title; - let draftContent = localStorage.getItem(this.currentNote.filename); + let draftContent = localStorage.getItem(this.currentNote.title); if (draftContent) { this.$bvModal @@ -227,7 +227,7 @@ export default { parent.initialContent = draftContent; } else { parent.initialContent = parent.currentNote.content; - localStorage.removeItem(parent.currentNote.filename); + localStorage.removeItem(parent.currentNote.title); } parent.editMode = !parent.editMode; }); @@ -266,10 +266,10 @@ export default { }, saveDraft: function() { - localStorage.setItem(this.currentNote.filename, this.getEditorContent()); + localStorage.setItem(this.currentNote.title, this.getEditorContent()); }, - existingFilenameToast: function() { + existingTitleToast: function() { this.$bvToast.toast( "A note with this title already exists. Please try again with a new title.", { @@ -300,7 +300,7 @@ export default { if (this.currentNote.lastModified == null) { api .post(`/api/notes`, { - filename: `${this.titleInput}.${constants.markdownExt}`, + title: this.titleInput, content: newContent, }) .then(this.saveNoteResponseHandler) @@ -311,7 +311,7 @@ export default { typeof error.response !== "undefined" && error.response.status == 409 ) { - parent.existingFilenameToast(); + parent.existingTitleToast(); } else { parent.unhandledServerErrorToast(); } @@ -324,8 +324,8 @@ export default { this.titleInput != this.currentNote.title ) { api - .patch(`/api/notes/${this.currentNote.filename}`, { - newFilename: `${this.titleInput}.${this.currentNote.ext}`, + .patch(`/api/notes/${this.currentNote.title}`, { + newTitle: this.titleInput, newContent: newContent, }) .then(this.saveNoteResponseHandler) @@ -336,7 +336,7 @@ export default { typeof error.response !== "undefined" && error.response.status == 409 ) { - parent.existingFilenameToast(); + parent.existingTitleToast(); } else { parent.unhandledServerErrorToast(); } @@ -351,9 +351,9 @@ export default { }, saveNoteResponseHandler: function(response) { - localStorage.removeItem(this.currentNote.filename); + localStorage.removeItem(this.currentNote.title); this.currentNote = new Note( - response.data.filename, + response.data.title, response.data.lastModified, response.data.content ); @@ -371,7 +371,7 @@ export default { }, cancelNote: function() { - localStorage.removeItem(this.currentNote.filename); + localStorage.removeItem(this.currentNote.title); if (this.currentNote.lastModified == null) { // Cancelling a new note this.currentNote = null; @@ -395,7 +395,7 @@ export default { .then(function(response) { if (response == true) { api - .delete(`/api/notes/${parent.currentNote.filename}`) + .delete(`/api/notes/${parent.currentNote.title}`) .then(function() { parent.navigate("/"); parent.$bvToast.toast("Note deleted ✓", { diff --git a/flatnotes/src/components/App.vue b/flatnotes/src/components/App.vue index 141befe..54867e1 100644 --- a/flatnotes/src/components/App.vue +++ b/flatnotes/src/components/App.vue @@ -204,11 +204,7 @@
-