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 Mar 3, 2023
1 parent dff49b3 commit 570c165
Show file tree
Hide file tree
Showing 15 changed files with 260 additions and 80 deletions.
16 changes: 11 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 @@ -39,18 +44,19 @@ FROM python:3.11-slim

# set up the system
RUN apt update && \
apt install --yes nginx dumb-init libmagic1 && \
apt install --yes nginx dumb-init libmagic1 gettext && \
rm -rf /var/lib/apt/lists/*

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/sites-enabled/default
# substitute the prefix path in the nginx config
RUN envsubst < docat/nginx/default > /etc/nginx/sites-enabled/default

# 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
59 changes: 32 additions & 27 deletions docat/docat/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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 +48,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 +66,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 +82,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 +118,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 +196,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 +237,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 +277,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 +316,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 +364,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 +383,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 +410,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 +461,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 +504,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 @@ -66,4 +66,4 @@
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.32.1"
}
}
}
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 navigateToSearchPage = (): void => {
if (linkRef.current != null) {
linkRef.current.click()
}
}

return (
<>
<Search
className={styles['search-icon']}
onClick={() => navigate('/search')}
/>
<Search className={styles['search-icon']} onClick={navigateToSearchPage} />
<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}`)
navigateToSearchPage()
}
}}
variant="standard"
></TextField>
</div>

<Link ref={linkRef} to={`/search?query=${searchQuery}`} />
</>
)
}
8 changes: 5 additions & 3 deletions web/src/data-providers/ConfigDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import React, { createContext, useContext, useEffect, useState } from 'react'

import ProjectRepository from '../repositories/ProjectRepository'
export interface Config {
headerHTML?: string
}
Expand All @@ -22,8 +22,10 @@ export const ConfigDataProvider = ({ children }: any): JSX.Element => {
useEffect(() => {
void (async () => {
try {
const res = await fetch('/doc/config.json')
const data = await res.json() as Config
const res = await fetch(
`${ProjectRepository.getURLPrefix()}/doc/config.json`
)
const data = (await res.json()) as Config
setConfig(data)
} catch (err) {
console.error(err)
Expand Down
Loading

0 comments on commit 570c165

Please sign in to comment.