structables 0.4.0__tar.gz → 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- structables-0.4.3/.dockerignore +15 -0
- {structables-0.4.0 → structables-0.4.3}/.forgejo/workflows/docker.yml +4 -6
- {structables-0.4.0 → structables-0.4.3}/.forgejo/workflows/release.yml +1 -0
- {structables-0.4.0 → structables-0.4.3}/.gitignore +1 -0
- {structables-0.4.0 → structables-0.4.3}/Dockerfile +2 -3
- {structables-0.4.0 → structables-0.4.3}/PKG-INFO +10 -7
- {structables-0.4.0 → structables-0.4.3}/README.md +8 -5
- {structables-0.4.0 → structables-0.4.3}/docker-compose-example-dev.yml +2 -1
- {structables-0.4.0 → structables-0.4.3}/docker-compose-example.yml +3 -2
- {structables-0.4.0 → structables-0.4.3}/instances.json +10 -0
- {structables-0.4.0 → structables-0.4.3}/pyproject.toml +2 -2
- {structables-0.4.0 → structables-0.4.3}/src/structables/config.py +19 -4
- structables-0.4.3/src/structables/main.py +46 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/routes/main.py +7 -1
- {structables-0.4.0 → structables-0.4.3}/src/structables/routes/proxy.py +54 -44
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/base.html +24 -4
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/header.html +0 -8
- {structables-0.4.0 → structables-0.4.3}/src/structables/utils/data.py +24 -1
- structables-0.4.0/src/structables/main.py +0 -65
- {structables-0.4.0 → structables-0.4.3}/.env.example +0 -0
- {structables-0.4.0 → structables-0.4.3}/LICENSE +0 -0
- {structables-0.4.0 → structables-0.4.3}/cronjob.txt +0 -0
- {structables-0.4.0 → structables-0.4.3}/entrypoint.sh +0 -0
- {structables-0.4.0 → structables-0.4.3}/renovate.json +0 -0
- {structables-0.4.0 → structables-0.4.3}/requirements-dev.txt +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/__init__.py +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/routes/__init__.py +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/routes/category.py +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/routes/contest.py +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/routes/member.py +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/static/css/iframe.css +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/static/css/style.css +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/static/img/logo.png +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/400.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/404.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/429.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/500.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/archives.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/article-review.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/article.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/category.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/collection.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/contest.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/contests.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/footer.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/iframe.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/index.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/member-instructables.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/member.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/projects.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/sitemap.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/templates/style.html +0 -0
- {structables-0.4.0 → structables-0.4.3}/src/structables/utils/helpers.py +0 -0
|
@@ -4,17 +4,15 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
6
|
- "*"
|
|
7
|
+
workflow_dispatch:
|
|
7
8
|
|
|
8
9
|
jobs:
|
|
9
10
|
docker:
|
|
10
11
|
name: Docker Build and Push to Docker Hub
|
|
12
|
+
runs-on: docker-privileged
|
|
11
13
|
container:
|
|
12
|
-
image:
|
|
14
|
+
image: catthehacker/ubuntu:act-latest
|
|
13
15
|
steps:
|
|
14
|
-
- name: Install dependencies
|
|
15
|
-
run: |
|
|
16
|
-
apt update
|
|
17
|
-
apt install -y docker.io
|
|
18
16
|
- name: Set up QEMU
|
|
19
17
|
uses: docker/setup-qemu-action@v3
|
|
20
18
|
- name: Set up Docker Buildx
|
|
@@ -30,4 +28,4 @@ jobs:
|
|
|
30
28
|
push: true
|
|
31
29
|
tags: |
|
|
32
30
|
privatecoffee/structables:latest
|
|
33
|
-
privatecoffee/structables:${{
|
|
31
|
+
privatecoffee/structables:${{ github.ref_name }}
|
|
@@ -2,6 +2,7 @@ FROM alpine:3.21
|
|
|
2
2
|
|
|
3
3
|
ENV APP_ENV=/opt/venv
|
|
4
4
|
ENV PATH="${APP_ENV}/bin:$PATH"
|
|
5
|
+
ENV PORT=8002
|
|
5
6
|
|
|
6
7
|
RUN apk add --no-cache py3-pip uwsgi-python3 && \
|
|
7
8
|
python3 -m venv $APP_ENV
|
|
@@ -12,10 +13,8 @@ RUN $APP_ENV/bin/pip install --no-cache-dir pip && \
|
|
|
12
13
|
$APP_ENV/bin/pip install /app && \
|
|
13
14
|
adduser -S -D -H structables
|
|
14
15
|
|
|
15
|
-
COPY entrypoint.sh /entrypoint.sh
|
|
16
|
-
|
|
17
16
|
EXPOSE 8002
|
|
18
17
|
|
|
19
18
|
USER structables
|
|
20
19
|
|
|
21
|
-
ENTRYPOINT ["/entrypoint.sh"]
|
|
20
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structables
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: A simple frontend for Instructables
|
|
5
5
|
Project-URL: Homepage, https://git.private.coffee/privatecoffee/structables
|
|
6
6
|
Project-URL: Bug Tracker, https://git.private.coffee/privatecoffee/structables/issues
|
|
@@ -672,7 +672,7 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
|
672
672
|
Classifier: Operating System :: OS Independent
|
|
673
673
|
Classifier: Programming Language :: Python :: 3
|
|
674
674
|
Requires-Python: >=3.10
|
|
675
|
-
Requires-Dist:
|
|
675
|
+
Requires-Dist: beautifulsoup4
|
|
676
676
|
Requires-Dist: flask
|
|
677
677
|
Requires-Dist: markdown2[all]
|
|
678
678
|
Description-Content-Type: text/markdown
|
|
@@ -694,11 +694,12 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
694
694
|
|
|
695
695
|
<!-- START_INSTANCE_LIST type:eq=clearnet -->
|
|
696
696
|
|
|
697
|
-
| URL
|
|
698
|
-
|
|
|
699
|
-
| [structables.private.coffee](https://structables.private.coffee)
|
|
700
|
-
| [structables.bloat.cat](https://structables.bloat.cat)
|
|
701
|
-
| [structables.darkness.services](https://structables.darkness.services)
|
|
697
|
+
| URL | Provided by | Country | Notes |
|
|
698
|
+
| -------------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
|
|
699
|
+
| [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
|
|
700
|
+
| [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
|
|
701
|
+
| [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
|
|
702
|
+
| [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
|
|
702
703
|
|
|
703
704
|
<!-- END_INSTANCE_LIST -->
|
|
704
705
|
|
|
@@ -737,6 +738,8 @@ Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structa
|
|
|
737
738
|
|
|
738
739
|
### Production: Docker
|
|
739
740
|
|
|
741
|
+
**Note:** The Docker image is now hosted on [git.private.coffee](https://git.private.coffee/privatecoffee/structables/packages) instead of Docker Hub. Please use the new image name `git.private.coffee/privatecoffee/structables` instead of `privatecoffee/structables` to continue receiving updates.
|
|
742
|
+
|
|
740
743
|
1. Copy `.env.example` to `.env` and adjust the settings as necessary
|
|
741
744
|
2. Copy `docker-compose-example.yml` to `docker-compose.yml` and adjust it as necessary, for example modifying resource limits or changing the port/host configuration
|
|
742
745
|
3. Build and run the Docker container:
|
|
@@ -15,11 +15,12 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
15
15
|
|
|
16
16
|
<!-- START_INSTANCE_LIST type:eq=clearnet -->
|
|
17
17
|
|
|
18
|
-
| URL
|
|
19
|
-
|
|
|
20
|
-
| [structables.private.coffee](https://structables.private.coffee)
|
|
21
|
-
| [structables.bloat.cat](https://structables.bloat.cat)
|
|
22
|
-
| [structables.darkness.services](https://structables.darkness.services)
|
|
18
|
+
| URL | Provided by | Country | Notes |
|
|
19
|
+
| -------------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
|
|
20
|
+
| [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
|
|
21
|
+
| [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
|
|
22
|
+
| [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
|
|
23
|
+
| [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
|
|
23
24
|
|
|
24
25
|
<!-- END_INSTANCE_LIST -->
|
|
25
26
|
|
|
@@ -58,6 +59,8 @@ Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structa
|
|
|
58
59
|
|
|
59
60
|
### Production: Docker
|
|
60
61
|
|
|
62
|
+
**Note:** The Docker image is now hosted on [git.private.coffee](https://git.private.coffee/privatecoffee/structables/packages) instead of Docker Hub. Please use the new image name `git.private.coffee/privatecoffee/structables` instead of `privatecoffee/structables` to continue receiving updates.
|
|
63
|
+
|
|
61
64
|
1. Copy `.env.example` to `.env` and adjust the settings as necessary
|
|
62
65
|
2. Copy `docker-compose-example.yml` to `docker-compose.yml` and adjust it as necessary, for example modifying resource limits or changing the port/host configuration
|
|
63
66
|
3. Build and run the Docker container:
|
|
@@ -2,15 +2,16 @@ services:
|
|
|
2
2
|
structables:
|
|
3
3
|
container_name: structables
|
|
4
4
|
restart: unless-stopped
|
|
5
|
-
image: privatecoffee/structables:latest
|
|
5
|
+
image: git.private.coffee/privatecoffee/structables:latest
|
|
6
6
|
ports:
|
|
7
7
|
- "127.0.0.1:8002:8002"
|
|
8
|
+
volumes:
|
|
9
|
+
- ./cache:/app/structables_cache
|
|
8
10
|
env_file: .env
|
|
9
11
|
security_opt:
|
|
10
12
|
- no-new-privileges:true
|
|
11
13
|
cap_drop:
|
|
12
14
|
- ALL
|
|
13
|
-
read_only: true
|
|
14
15
|
deploy:
|
|
15
16
|
resources:
|
|
16
17
|
limits:
|
|
@@ -50,5 +50,15 @@
|
|
|
50
50
|
"url": "https://darkness.services"
|
|
51
51
|
},
|
|
52
52
|
"location": "US"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "structables.franklyflawless.org",
|
|
56
|
+
"url": "https://structables.franklyflawless.org",
|
|
57
|
+
"type": "clearnet",
|
|
58
|
+
"provider": {
|
|
59
|
+
"name": "FranklyFlawless",
|
|
60
|
+
"url": "https://franklyflawless.org"
|
|
61
|
+
},
|
|
62
|
+
"location": "DE"
|
|
53
63
|
}
|
|
54
64
|
]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structables"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Private.coffee Team", email="support@private.coffee" },
|
|
10
10
|
]
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
21
|
"flask",
|
|
22
|
-
"
|
|
22
|
+
"beautifulsoup4",
|
|
23
23
|
"markdown2[all]",
|
|
24
24
|
]
|
|
25
25
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
3
6
|
|
|
4
7
|
from .utils.helpers import get_typesense_api_key
|
|
5
8
|
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
class Config:
|
|
8
13
|
DEBUG = os.environ.get("FLASK_DEBUG", os.environ.get("STRUCTABLES_DEBUG", False))
|
|
@@ -24,10 +29,20 @@ class Config:
|
|
|
24
29
|
)
|
|
25
30
|
CACHE_DIR = os.environ.get("STRUCTABLES_CACHE_DIR")
|
|
26
31
|
|
|
27
|
-
if CACHE_DIR is None:
|
|
28
|
-
|
|
29
|
-
tempfile.gettempdir()
|
|
30
|
-
|
|
32
|
+
if CACHE_ENABLED and CACHE_DIR is None:
|
|
33
|
+
try:
|
|
34
|
+
CACHE_DIR = Path(tempfile.gettempdir()) / "structables_cache"
|
|
35
|
+
except FileNotFoundError:
|
|
36
|
+
CACHE_DIR = Path(os.getcwd()) / "structables_cache"
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
(CACHE_DIR / "test").write_text("test")
|
|
41
|
+
(CACHE_DIR / "test").unlink()
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.error(f"Could not create or write to cache directory: {e} - disabling cache")
|
|
44
|
+
CACHE_ENABLED = False
|
|
45
|
+
CACHE_DIR = None
|
|
31
46
|
|
|
32
47
|
CACHE_MAX_AGE = int(
|
|
33
48
|
os.environ.get("STRUCTABLES_CACHE_MAX_AGE", 60 * 60 * 24 * 7)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from flask import Flask
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .routes import init_routes
|
|
9
|
+
from .utils.data import maybe_update_data
|
|
10
|
+
|
|
11
|
+
# Configure logging
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
15
|
+
app.config.from_object(Config)
|
|
16
|
+
|
|
17
|
+
logger.debug("Initializing routes")
|
|
18
|
+
init_routes(app)
|
|
19
|
+
logger.debug("Performing initial data update")
|
|
20
|
+
maybe_update_data(app)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
if app.config["DEBUG"]:
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
level=logging.DEBUG,
|
|
27
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
logging.basicConfig(
|
|
31
|
+
level=logging.INFO,
|
|
32
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger.info(
|
|
36
|
+
f"Starting Structables on {app.config['LISTEN_HOST']}:{app.config['PORT']}"
|
|
37
|
+
)
|
|
38
|
+
app.run(
|
|
39
|
+
port=app.config["PORT"],
|
|
40
|
+
host=app.config["LISTEN_HOST"],
|
|
41
|
+
debug=app.config["DEBUG"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -6,6 +6,7 @@ from urllib.parse import quote
|
|
|
6
6
|
from werkzeug.exceptions import InternalServerError
|
|
7
7
|
from markdown2 import Markdown
|
|
8
8
|
from traceback import print_exc
|
|
9
|
+
|
|
9
10
|
import pathlib
|
|
10
11
|
import json
|
|
11
12
|
import logging
|
|
@@ -13,6 +14,7 @@ import logging
|
|
|
13
14
|
from ..utils.data import update_data
|
|
14
15
|
from ..utils.helpers import explore_lists, proxy
|
|
15
16
|
from .category import project_list
|
|
17
|
+
from ..utils.data import maybe_update_data
|
|
16
18
|
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
@@ -22,6 +24,9 @@ def init_main_routes(app):
|
|
|
22
24
|
def route_explore():
|
|
23
25
|
logger.debug("Rendering explore page")
|
|
24
26
|
|
|
27
|
+
# Check if data needs to be updated
|
|
28
|
+
maybe_update_data(app)
|
|
29
|
+
|
|
25
30
|
try:
|
|
26
31
|
logger.debug("Fetching data from instructables.com")
|
|
27
32
|
data = urlopen("https://www.instructables.com/")
|
|
@@ -130,8 +135,9 @@ def init_main_routes(app):
|
|
|
130
135
|
|
|
131
136
|
try:
|
|
132
137
|
logger.debug(f"Fetching article data from instructables.com for: {article}")
|
|
138
|
+
article_path = quote(article)
|
|
133
139
|
data = urlopen(
|
|
134
|
-
f"https://www.instructables.com/json-api/showInstructableModel?urlString={
|
|
140
|
+
f"https://www.instructables.com/json-api/showInstructableModel?urlString={article_path}"
|
|
135
141
|
)
|
|
136
142
|
data = json.loads(data.read().decode())
|
|
137
143
|
logger.debug("Successfully fetched article data")
|
|
@@ -7,13 +7,12 @@ import logging
|
|
|
7
7
|
import os
|
|
8
8
|
import hashlib
|
|
9
9
|
import time
|
|
10
|
-
import threading
|
|
11
10
|
import shutil
|
|
12
11
|
|
|
13
12
|
logger = logging.getLogger(__name__)
|
|
14
13
|
|
|
15
|
-
#
|
|
16
|
-
|
|
14
|
+
# Track last cache cleanup time
|
|
15
|
+
last_cache_cleanup = 0
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
def get_cache_path(app, url):
|
|
@@ -94,24 +93,35 @@ def get_content_type(cache_path):
|
|
|
94
93
|
return "application/octet-stream"
|
|
95
94
|
|
|
96
95
|
|
|
97
|
-
def
|
|
98
|
-
"""Clean up the cache directory
|
|
99
|
-
|
|
100
|
-
This function removes the oldest files first until the cache size
|
|
101
|
-
is below the maximum size.
|
|
96
|
+
def maybe_cleanup_cache(app):
|
|
97
|
+
"""Clean up the cache directory if it's time to do so.
|
|
102
98
|
|
|
103
99
|
Args:
|
|
104
100
|
app: The Flask app instance.
|
|
105
101
|
"""
|
|
102
|
+
global last_cache_cleanup
|
|
103
|
+
|
|
106
104
|
# If caching is disabled, don't do anything
|
|
107
105
|
if not app.config["CACHE_ENABLED"]:
|
|
108
106
|
return
|
|
109
107
|
|
|
108
|
+
# Check if it's time to run cleanup
|
|
109
|
+
current_time = time.time()
|
|
110
|
+
cleanup_interval = app.config["CACHE_CLEANUP_INTERVAL"]
|
|
111
|
+
|
|
112
|
+
if current_time - last_cache_cleanup < cleanup_interval:
|
|
113
|
+
logger.debug(
|
|
114
|
+
f"Cache cleanup skipped. Time since last cleanup: {current_time - last_cache_cleanup:.2f} seconds"
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
|
|
110
118
|
logger.debug("Starting cache cleanup")
|
|
119
|
+
last_cache_cleanup = current_time
|
|
111
120
|
|
|
112
121
|
try:
|
|
113
122
|
cache_dir = app.config["CACHE_DIR"]
|
|
114
123
|
max_size = app.config["CACHE_MAX_SIZE"]
|
|
124
|
+
max_age = app.config["CACHE_MAX_AGE"]
|
|
115
125
|
|
|
116
126
|
# Get all cache files with their modification times
|
|
117
127
|
cache_files = []
|
|
@@ -120,6 +130,10 @@ def cache_cleanup(app):
|
|
|
120
130
|
for filename in os.listdir(cache_dir):
|
|
121
131
|
file_path = os.path.join(cache_dir, filename)
|
|
122
132
|
if os.path.isfile(file_path):
|
|
133
|
+
# Skip metadata files in the count
|
|
134
|
+
if file_path.endswith(".meta"):
|
|
135
|
+
continue
|
|
136
|
+
|
|
123
137
|
file_size = os.path.getsize(file_path)
|
|
124
138
|
file_time = os.path.getmtime(file_path)
|
|
125
139
|
total_size += file_size
|
|
@@ -127,7 +141,34 @@ def cache_cleanup(app):
|
|
|
127
141
|
|
|
128
142
|
logger.debug(f"Current cache size: {total_size / (1024 * 1024):.2f} MB")
|
|
129
143
|
|
|
130
|
-
#
|
|
144
|
+
# First, remove expired files
|
|
145
|
+
current_time = time.time()
|
|
146
|
+
expired_files = [
|
|
147
|
+
(path, mtime, size)
|
|
148
|
+
for path, mtime, size in cache_files
|
|
149
|
+
if current_time - mtime > max_age
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
for file_path, _, file_size in expired_files:
|
|
153
|
+
try:
|
|
154
|
+
os.remove(file_path)
|
|
155
|
+
# Also remove metadata file if it exists
|
|
156
|
+
meta_path = file_path + ".meta"
|
|
157
|
+
if os.path.exists(meta_path):
|
|
158
|
+
os.remove(meta_path)
|
|
159
|
+
total_size -= file_size
|
|
160
|
+
logger.debug(f"Removed expired cache file: {file_path}")
|
|
161
|
+
except OSError:
|
|
162
|
+
logger.warning(f"Failed to remove expired cache file: {file_path}")
|
|
163
|
+
|
|
164
|
+
# Remove files from the list that we've already deleted
|
|
165
|
+
cache_files = [
|
|
166
|
+
(path, mtime, size)
|
|
167
|
+
for path, mtime, size in cache_files
|
|
168
|
+
if (path, mtime, size) not in expired_files
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
# If we're still over the size limit, remove oldest files first
|
|
131
172
|
if total_size > max_size:
|
|
132
173
|
logger.debug("Cache size exceeds limit, cleaning up")
|
|
133
174
|
# Sort by modification time (oldest first)
|
|
@@ -146,7 +187,7 @@ def cache_cleanup(app):
|
|
|
146
187
|
os.remove(meta_path)
|
|
147
188
|
|
|
148
189
|
total_size -= file_size
|
|
149
|
-
logger.debug(f"Removed cache file: {file_path}")
|
|
190
|
+
logger.debug(f"Removed old cache file: {file_path}")
|
|
150
191
|
except OSError:
|
|
151
192
|
logger.warning(f"Failed to remove cache file: {file_path}")
|
|
152
193
|
|
|
@@ -158,40 +199,6 @@ def cache_cleanup(app):
|
|
|
158
199
|
logger.error(f"Error during cache cleanup: {str(e)}")
|
|
159
200
|
|
|
160
201
|
|
|
161
|
-
def start_cache_cleanup_thread(app):
|
|
162
|
-
"""Start a background thread to periodically clean up the cache.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
app: The Flask app instance.
|
|
166
|
-
"""
|
|
167
|
-
global cache_cleanup_thread
|
|
168
|
-
|
|
169
|
-
# If thread is already running, don't start another one
|
|
170
|
-
if cache_cleanup_thread is not None and cache_cleanup_thread.is_alive():
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
# If caching is disabled, don't start the thread
|
|
174
|
-
if not app.config["CACHE_ENABLED"]:
|
|
175
|
-
logger.debug("Caching is disabled, not starting cache cleanup thread")
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
def cleanup_worker():
|
|
179
|
-
while True:
|
|
180
|
-
try:
|
|
181
|
-
with app.app_context():
|
|
182
|
-
cache_cleanup(app)
|
|
183
|
-
cleanup_interval = app.config["CACHE_CLEANUP_INTERVAL"]
|
|
184
|
-
time.sleep(cleanup_interval)
|
|
185
|
-
except Exception as e:
|
|
186
|
-
logger.error(f"Error in cache cleanup worker: {str(e)}")
|
|
187
|
-
# Sleep a bit to avoid tight loop in case of recurring errors
|
|
188
|
-
time.sleep(60)
|
|
189
|
-
|
|
190
|
-
cache_cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
|
|
191
|
-
cache_cleanup_thread.start()
|
|
192
|
-
logger.debug("Started cache cleanup background thread")
|
|
193
|
-
|
|
194
|
-
|
|
195
202
|
def init_proxy_routes(app):
|
|
196
203
|
# Create cache directory if it doesn't exist and caching is enabled
|
|
197
204
|
if app.config["CACHE_ENABLED"]:
|
|
@@ -215,6 +222,9 @@ def init_proxy_routes(app):
|
|
|
215
222
|
|
|
216
223
|
logger.debug(f"Proxy request for URL: {url}, filename: {filename}")
|
|
217
224
|
|
|
225
|
+
# Clean up the cache if needed
|
|
226
|
+
maybe_cleanup_cache(app)
|
|
227
|
+
|
|
218
228
|
if url is not None:
|
|
219
229
|
if url.startswith("https://cdn.instructables.com/") or url.startswith(
|
|
220
230
|
"https://content.instructables.com/"
|
|
@@ -21,21 +21,41 @@
|
|
|
21
21
|
{% include "footer.html" %}
|
|
22
22
|
{% if config["THEME"] == "auto" %}
|
|
23
23
|
<script>
|
|
24
|
+
// Create theme switcher elements
|
|
25
|
+
const themeSwitchWrapper = document.querySelector('.theme-switch-wrapper');
|
|
26
|
+
const sunIcon = document.createElement('span');
|
|
27
|
+
sunIcon.className = 'theme-icon sun-icon';
|
|
28
|
+
const moonIcon = document.createElement('span');
|
|
29
|
+
moonIcon.className = 'theme-icon moon-icon';
|
|
30
|
+
const themeSwitchLabel = document.createElement('label');
|
|
31
|
+
themeSwitchLabel.className = 'theme-switch';
|
|
32
|
+
const themeSwitchInput = document.createElement('input');
|
|
33
|
+
themeSwitchInput.type = 'checkbox';
|
|
34
|
+
themeSwitchInput.id = 'checkbox';
|
|
35
|
+
const sliderDiv = document.createElement('div');
|
|
36
|
+
sliderDiv.className = 'slider';
|
|
37
|
+
|
|
38
|
+
// Append elements to the theme switch wrapper
|
|
39
|
+
themeSwitchLabel.appendChild(themeSwitchInput);
|
|
40
|
+
themeSwitchLabel.appendChild(sliderDiv);
|
|
41
|
+
themeSwitchWrapper.appendChild(sunIcon);
|
|
42
|
+
themeSwitchWrapper.appendChild(themeSwitchLabel);
|
|
43
|
+
themeSwitchWrapper.appendChild(moonIcon);
|
|
44
|
+
|
|
24
45
|
// Theme toggle functionality
|
|
25
|
-
const toggleSwitch = document.querySelector('#checkbox');
|
|
26
46
|
const currentTheme = localStorage.getItem('theme');
|
|
27
47
|
|
|
28
48
|
// Set theme based on saved preference or system preference
|
|
29
49
|
if (currentTheme) {
|
|
30
50
|
document.documentElement.setAttribute('data-theme', currentTheme);
|
|
31
51
|
if (currentTheme === 'dark') {
|
|
32
|
-
|
|
52
|
+
themeSwitchInput.checked = true;
|
|
33
53
|
}
|
|
34
54
|
} else {
|
|
35
55
|
// Check if user prefers dark mode
|
|
36
56
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
37
57
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
38
|
-
|
|
58
|
+
themeSwitchInput.checked = true;
|
|
39
59
|
localStorage.setItem('theme', 'dark');
|
|
40
60
|
}
|
|
41
61
|
}
|
|
@@ -51,7 +71,7 @@
|
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
|
|
74
|
+
themeSwitchInput.addEventListener('change', switchTheme, false);
|
|
55
75
|
</script>
|
|
56
76
|
{% endif %}
|
|
57
77
|
</body>
|
|
@@ -20,16 +20,8 @@
|
|
|
20
20
|
<li class="nav-item">
|
|
21
21
|
<a class="nav-link" href="/sitemap/">Sitemap</a>
|
|
22
22
|
</li>
|
|
23
|
-
{% if config["THEME"] == "auto" %}
|
|
24
23
|
<li class="nav-item theme-switch-wrapper">
|
|
25
|
-
<span class="theme-icon sun-icon"></span>
|
|
26
|
-
<label class="theme-switch" for="checkbox">
|
|
27
|
-
<input type="checkbox" id="checkbox" />
|
|
28
|
-
<div class="slider"></div>
|
|
29
|
-
</label>
|
|
30
|
-
<span class="theme-icon moon-icon"></span>
|
|
31
24
|
</li>
|
|
32
|
-
{% endif %}
|
|
33
25
|
</ul>
|
|
34
26
|
<form class="search-form" action="/search" method="post">
|
|
35
27
|
<input class="search-input"
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from urllib.request import urlopen
|
|
2
2
|
import logging
|
|
3
|
+
import time
|
|
3
4
|
from bs4 import BeautifulSoup
|
|
4
5
|
from .helpers import proxy, projects_search
|
|
5
6
|
|
|
6
7
|
logger = logging.getLogger(__name__)
|
|
7
8
|
|
|
9
|
+
# Track the last data update time
|
|
10
|
+
last_data_update = 0
|
|
11
|
+
|
|
8
12
|
def update_data(app):
|
|
9
13
|
"""Update the application's cached data.
|
|
10
14
|
|
|
@@ -73,4 +77,23 @@ def update_data(app):
|
|
|
73
77
|
logger.debug(f"Updated global projects list with {len(app.global_ibles['/projects'])} projects")
|
|
74
78
|
logger.debug("Data update completed successfully")
|
|
75
79
|
except Exception as e:
|
|
76
|
-
logger.error(f"Error updating data: {str(e)}")
|
|
80
|
+
logger.error(f"Error updating data: {str(e)}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def maybe_update_data(app):
|
|
84
|
+
"""Updates the data if it's time to do so.
|
|
85
|
+
|
|
86
|
+
This replaces the background thread with a request-triggered update.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
app (Flask): The Flask app instance.
|
|
90
|
+
"""
|
|
91
|
+
global last_data_update
|
|
92
|
+
current_time = time.time()
|
|
93
|
+
|
|
94
|
+
# Update every 5 minutes (300 seconds)
|
|
95
|
+
if current_time - last_data_update >= 300:
|
|
96
|
+
logger.debug("Running scheduled data update")
|
|
97
|
+
update_data(app)
|
|
98
|
+
last_data_update = current_time
|
|
99
|
+
logger.debug("Data update complete")
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
|
|
3
|
-
from flask import Flask
|
|
4
|
-
import threading
|
|
5
|
-
import time
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
from .config import Config
|
|
9
|
-
from .routes import init_routes
|
|
10
|
-
from .utils.data import update_data
|
|
11
|
-
from .routes.proxy import start_cache_cleanup_thread
|
|
12
|
-
|
|
13
|
-
# Configure logging
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
17
|
-
app.config.from_object(Config)
|
|
18
|
-
|
|
19
|
-
logger.debug("Initializing routes")
|
|
20
|
-
init_routes(app)
|
|
21
|
-
logger.debug("Performing initial data update")
|
|
22
|
-
update_data(app)
|
|
23
|
-
|
|
24
|
-
def background_update_data(app):
|
|
25
|
-
"""Runs the update_data function every 5 minutes.
|
|
26
|
-
|
|
27
|
-
This replaces the need for a cron job to update the data.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
app (Flask): The Flask app instance.
|
|
31
|
-
"""
|
|
32
|
-
logger.debug("Starting background update thread")
|
|
33
|
-
while True:
|
|
34
|
-
logger.debug("Running scheduled data update")
|
|
35
|
-
update_data(app)
|
|
36
|
-
logger.debug("Data update complete, sleeping for 5 minutes")
|
|
37
|
-
time.sleep(300)
|
|
38
|
-
|
|
39
|
-
def main():
|
|
40
|
-
if app.config["DEBUG"]:
|
|
41
|
-
logging.basicConfig(
|
|
42
|
-
level=logging.DEBUG,
|
|
43
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
44
|
-
)
|
|
45
|
-
else:
|
|
46
|
-
logging.basicConfig(
|
|
47
|
-
level=logging.INFO,
|
|
48
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
logger.debug("Starting background update thread")
|
|
52
|
-
threading.Thread(target=background_update_data, args=(app,), daemon=True).start()
|
|
53
|
-
|
|
54
|
-
# Start the cache cleanup thread
|
|
55
|
-
start_cache_cleanup_thread(app)
|
|
56
|
-
|
|
57
|
-
logger.info(f"Starting Structables on {app.config['LISTEN_HOST']}:{app.config['PORT']}")
|
|
58
|
-
app.run(
|
|
59
|
-
port=app.config["PORT"],
|
|
60
|
-
host=app.config["LISTEN_HOST"],
|
|
61
|
-
debug=app.config["DEBUG"],
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
if __name__ == "__main__":
|
|
65
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structables-0.4.0 → structables-0.4.3}/src/structables/static/img/magnifying-glass-solid.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|