Skip to content

Commit

Permalink
Add option for a prefix path
Browse files Browse the repository at this point in the history
You can now add a prefix path for docat (eg. https://domain.com/docat as
the root url).
Just follow the instructions in the readme

fixes: #80
  • Loading branch information
reglim committed Jan 23, 2023
1 parent 155f8f5 commit 5fdc86c
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 80 deletions.
19 changes: 14 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@

# building frontend
FROM node:16.14 as frontend
WORKDIR /app/frontend

ARG PREFIX_PATH_ARG=""
ENV PREFIX_PATH=$PREFIX_PATH_ARG
ENV REACT_APP_PREFIX_PATH=$PREFIX_PATH_ARG

COPY web ./

# fix docker not following symlinks
Expand All @@ -12,7 +18,6 @@ RUN yarn lint
# fix test not exiting by default
ARG CI=true
RUN yarn test

RUN yarn build

# setup Python
Expand All @@ -37,21 +42,25 @@ RUN poetry install --no-root --no-ansi --only main

# production
FROM python:3.11.0-alpine3.15
ARG PREFIX_PATH_ARG=""
ENV PREFIX_PATH=$PREFIX_PATH_ARG

# set up the system
RUN apk update && \
apk add nginx dumb-init libmagic && \
apk add nginx dumb-init libmagic gettext && \
rm -rf /var/cache/apk/*

RUN mkdir -p /var/docat/doc

# install the application
RUN mkdir -p /var/www/html
COPY --from=frontend /app/frontend/build /var/www/html
RUN mkdir -p /var/www/html${PREFIX_PATH}
COPY --from=frontend /app/frontend/build /var/www/html${PREFIX_PATH}
COPY docat /app/docat
WORKDIR /app/docat

RUN cp docat/nginx/default /etc/nginx/http.d/default.conf

# substitute the prefix path in the nginx config
RUN envsubst < docat/nginx/default > /etc/nginx/http.d/default.conf

# Copy the build artifact (.venv)
COPY --from=backend /app /app/docat
Expand Down
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,48 @@ Using `docatl`:
docatl update-index --host http://localhost:8000
```

Don't worry if it takes some time :)
Don't worry if it takes some time :)

## Adding a prefix path

It is possible to run docat on a subpath, for example `/docat/...`. To do this, you will need to perform different steps depending on how you want to run docat.

### Common Configuration

The first thing you will have to do no matter how you start docat, is to specify the prefix path in the `package.json` file (underneath the `version` tag):

> Important: The prefix path must start with a slash and end without one. (eg. `/docat`)
```json
"homepage": "/docat",
```

### Docker

To run docat with a prefix path in docker, you will need to rebuild the container with the build argument **PREFIX_PATH_ARG** set to your prefix path:

```sh
docker build . --no-cache --tag docat:with_prefix --build-arg PREFIX_PATH_ARG="/docat"

docker run --volume $DEV_DOCAT_PATH:/var/docat/ --publish 80:80 docat:with_prefix
```

### Local

To run docat locally with a prefix path, you have to specify the prefix path in environment variables for both the backend and the frontend:

#### Backend

You can run the backend directly with the **PREFIX_PATH** argument:

```sh
DOCAT_SERVE_FILES=1 DOCAT_STORAGE_PATH="$DEV_DOCAT_PATH" PREFIX_PATH="/docat" poetry run python -m docat
```

#### Frontend

For the frontend, you can just add the **REACT_APP_PREFIX_PATH** environment variable into the `.env` file:

```
REACT_APP_PREFIX_PATH=/docat
```
4 changes: 2 additions & 2 deletions docat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ Install the dependencies and run the application:
# install dependencies
poetry install
# run the app
[DOCAT_SERVE_FILES=1] [DOCAT_INDEX_FILES=1] [FLASK_DEBUG=1] [PORT=8888] poetry run python -m docat
[DOCAT_SERVE_FILES=1] [DOCAT_INDEX_FILES=1] [PORT=8888] poetry run python -m docat
```

### Config Options

* **DOCAT_SERVE_FILES**: Serve static documentation instead of a nginx (for testing)
* **DOCAT_INDEX_FILES**: Index files on start for searching
* **DOCAT_STORAGE_PATH**: Upload directory for static files (needs to match nginx config)
* **FLASK_DEBUG**: Start flask in debug mode
* **PREFIX_PATH**: Start FastAPI with a prefix path (eg. `/docat/api/projects/...`). -> needs to start with a slash and end without one

## Usage

Expand Down
68 changes: 41 additions & 27 deletions docat/docat/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@
from typing import Optional

import magic
from fastapi import Depends, FastAPI, File, Header, Response, UploadFile, status
from fastapi import (
APIRouter,
Depends,
FastAPI,
File,
Header,
Response,
UploadFile,
status,
)
from fastapi.staticfiles import StaticFiles
from starlette.responses import JSONResponse
from tinydb import Query, TinyDB
Expand Down Expand Up @@ -48,7 +57,10 @@
update_version_index_for_project,
)

#: Holds the FastAPI application
PREFIX_PATH = os.getenv("PREFIX_PATH", "")

router = APIRouter(prefix=PREFIX_PATH)

app = FastAPI(
title="docat",
description="API for docat, https://github.com/docat-org/docat",
Expand All @@ -63,7 +75,7 @@
DOCAT_UPLOAD_FOLDER = DOCAT_STORAGE_PATH / UPLOAD_FOLDER


@app.on_event("startup")
@router.on_event("startup")
def startup_create_folders():
# Create the folders if they don't exist
DOCAT_UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
Expand All @@ -79,28 +91,28 @@ def get_index_db() -> TinyDB:
return TinyDB(DOCAT_INDEX_PATH)


@app.post("/api/index/update", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.post("/api/index/update/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/index/update", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/index/update/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def update_index():
index_all_projects(DOCAT_UPLOAD_FOLDER, DOCAT_INDEX_PATH)

return ApiResponse(message="Successfully updated search index")


@app.get("/api/projects", response_model=Projects, status_code=status.HTTP_200_OK)
@router.get("/api/projects", response_model=Projects, status_code=status.HTTP_200_OK)
def get_projects(include_hidden: bool = False):
if not DOCAT_UPLOAD_FOLDER.exists():
return Projects(projects=[])
return get_all_projects(DOCAT_UPLOAD_FOLDER, include_hidden)


@app.get(
@router.get(
"/api/projects/{project}",
response_model=ProjectDetail,
status_code=status.HTTP_200_OK,
responses={status.HTTP_404_NOT_FOUND: {"model": ApiResponse}},
)
@app.get(
@router.get(
"/api/projects/{project}/",
response_model=ProjectDetail,
status_code=status.HTTP_200_OK,
Expand All @@ -115,8 +127,8 @@ def get_project(project, include_hidden: bool = False):
return details


@app.get("/api/search", response_model=SearchResponse, status_code=status.HTTP_200_OK)
@app.get("/api/search/", response_model=SearchResponse, status_code=status.HTTP_200_OK)
@router.get("/api/search", response_model=SearchResponse, status_code=status.HTTP_200_OK)
@router.get("/api/search/", response_model=SearchResponse, status_code=status.HTTP_200_OK)
def search(query: str, index_db: TinyDB = Depends(get_index_db)):
query = query.lower().strip()

Expand Down Expand Up @@ -193,8 +205,8 @@ def search(query: str, index_db: TinyDB = Depends(get_index_db)):
return SearchResponse(projects=found_projects, versions=found_versions, files=found_files)


@app.post("/api/{project}/icon", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.post("/api/{project}/icon/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/{project}/icon", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/{project}/icon/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def upload_icon(
project: str,
response: Response,
Expand Down Expand Up @@ -234,8 +246,8 @@ def upload_icon(
return ApiResponse(message="Icon successfully uploaded")


@app.post("/api/{project}/{version}/hide", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.post("/api/{project}/{version}/hide/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/{project}/{version}/hide", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/{project}/{version}/hide/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def hide_version(
project: str,
version: str,
Expand Down Expand Up @@ -274,8 +286,8 @@ def hide_version(
return ApiResponse(message=f"Version {version} is now hidden")


@app.post("/api/{project}/{version}/show", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.post("/api/{project}/{version}/show/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/{project}/{version}/show", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.post("/api/{project}/{version}/show/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def show_version(
project: str,
version: str,
Expand Down Expand Up @@ -313,8 +325,8 @@ def show_version(
return ApiResponse(message=f"Version {version} is now shown")


@app.post("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
@app.post("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
@router.post("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
@router.post("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
def upload(
project: str,
version: str,
Expand Down Expand Up @@ -361,8 +373,8 @@ def upload(
return ApiResponse(message="File successfully uploaded")


@app.put("/api/{project}/{version}/tags/{new_tag}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
@app.put("/api/{project}/{version}/tags/{new_tag}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
@router.put("/api/{project}/{version}/tags/{new_tag}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
@router.put("/api/{project}/{version}/tags/{new_tag}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
def tag(project: str, version: str, new_tag: str, response: Response, index_db: TinyDB = Depends(get_index_db)):
destination = DOCAT_UPLOAD_FOLDER / project / new_tag
source = DOCAT_UPLOAD_FOLDER / project / version
Expand All @@ -380,13 +392,13 @@ def tag(project: str, version: str, new_tag: str, response: Response, index_db:
return ApiResponse(message=f"Tag {new_tag} -> {version} successfully created")


@app.get(
@router.get(
"/api/{project}/claim",
response_model=ClaimResponse,
status_code=status.HTTP_201_CREATED,
responses={status.HTTP_409_CONFLICT: {"model": ApiResponse}},
)
@app.get(
@router.get(
"/api/{project}/claim/",
response_model=ClaimResponse,
status_code=status.HTTP_201_CREATED,
Expand All @@ -407,8 +419,8 @@ def claim(project: str, db: TinyDB = Depends(get_db)):
return ClaimResponse(message=f"Project {project} successfully claimed", token=token)


@app.put("/api/{project}/rename/{new_project_name}", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.put("/api/{project}/rename/{new_project_name}/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.put("/api/{project}/rename/{new_project_name}", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.put("/api/{project}/rename/{new_project_name}/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def rename(
project: str,
new_project_name: str,
Expand Down Expand Up @@ -458,8 +470,8 @@ def rename(
return ApiResponse(message=f"Successfully renamed project {project} to {new_project_name}")


@app.delete("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.delete("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.delete("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@router.delete("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def delete(
project: str,
version: str,
Expand Down Expand Up @@ -501,8 +513,10 @@ def check_token_for_project(db, token, project) -> TokenStatus:
# serve_local_docs for local testing without a nginx
if os.environ.get("DOCAT_SERVE_FILES"):
DOCAT_UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
app.mount("/doc", StaticFiles(directory=DOCAT_UPLOAD_FOLDER, html=True), name="docs")
app.mount(f"{PREFIX_PATH}/doc", StaticFiles(directory=DOCAT_UPLOAD_FOLDER, html=True), name="docs")

# index local files on start
if os.environ.get("DOCAT_INDEX_FILES"):
index_all_projects(DOCAT_UPLOAD_FOLDER, DOCAT_INDEX_PATH)

app.include_router(router)
14 changes: 9 additions & 5 deletions docat/docat/nginx/default
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ server {

server_name _;

location /doc {
root /var/docat;
location $PREFIX_PATH/doc {
# remove $PREFIX_PATH/doc from the request
rewrite ^$PREFIX_PATH/doc(.*) $1 break;

root /var/docat/doc;
}

location /api {
location $PREFIX_PATH/api {
client_max_body_size 100M;
proxy_pass http://python_backend;
}

location / {
location $PREFIX_PATH/ {

}
}
}
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.31.11"
}
}
}
19 changes: 12 additions & 7 deletions web/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { TextField } from '@mui/material'
import { useNavigate } from 'react-router-dom'
import { Link } from 'react-router-dom'
import React from 'react'
import styles from '../style/components/SearchBar.module.css'
import { Search } from '@mui/icons-material'

export default function SearchBar(): JSX.Element {
const navigate = useNavigate()
const linkRef = React.useRef<HTMLAnchorElement>(null)
const [searchQuery, setSearchQuery] = React.useState<string>('')

const navigate = (): void => {
if (linkRef.current != null) {
linkRef.current.click()
}
}

return (
<>
<Search
className={styles['search-icon']}
onClick={() => navigate('/search')}
/>
<Search className={styles['search-icon']} onClick={() => navigate()} />
<div className={styles['search-bar']}>
<TextField
label="Search"
Expand All @@ -22,12 +25,14 @@ export default function SearchBar(): JSX.Element {
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
navigate(`/search?query=${searchQuery}`)
navigate()
}
}}
variant="standard"
></TextField>
</div>

<Link ref={linkRef} to={`/search?query=${searchQuery}`} />
</>
)
}
Loading

0 comments on commit 5fdc86c

Please sign in to comment.