Skip to content

Commit 20a0b5f

Browse files
committed
Update project to work with Django 4 and maintained IPFS library
Update Python's IPFS client library Add tests (pytest) Modified minor things on readme and setup files
1 parent 44c298c commit 20a0b5f

11 files changed

+198
-130
lines changed

.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,3 @@ venv.bak/
104104

105105
# mypy
106106
.mypy_cache/
107-

.pre-commit-config.yaml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v2.2.3
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: flake8
8+
- id: check-merge-conflict
9+
- id: debug-statements
10+
- id: no-commit-to-branch
11+
12+
- repo: https://github.com/asottile/seed-isort-config
13+
rev: v1.9.2
14+
hooks:
15+
- id: seed-isort-config
16+
17+
- repo: https://github.com/ambv/black
18+
rev: 23.1.0
19+
hooks:
20+
- id: black
21+
language_version: python3.11
22+
23+
- repo: https://github.com/pre-commit/mirrors-isort
24+
rev: v4.3.20
25+
hooks:
26+
- id: isort

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Mozilla Public License Version 2.0
3535
means any form of the work other than Source Code Form.
3636

3737
1.7. "Larger Work"
38-
means a work that combines Covered Software with other material, in
38+
means a work that combines Covered Software with other material, in
3939
a separate file or files, that is not Covered Software.
4040

4141
1.8. "License"

README.md

+13-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
django-ipfs-storage
22
===================
33

4-
Store [Django file-uploads](https://docs.djangoproject.com/en/1.11/topics/files/)
4+
Store [Django file-uploads](https://docs.djangoproject.com/en/4.0/topics/files/)
55
on the [Interplanetary File System](https://ipfs.io/).
66

77
Uploads are added and pinned to the configured IPFS node,
@@ -10,9 +10,9 @@ This hash is the name that is saved to your database.
1010
Duplicate content will also have the same address,
1111
saving disk space.
1212

13-
Because of this only file creation and reading is supported.
13+
Because of this, only file creation and reading is supported.
1414

15-
Other IPFS users access and reseed a piece of content
15+
Other IPFS users access and reseed a piece of content
1616
through its unique content ID.
1717
Differently-distributed (i.e. normal HTTP) users
1818
can access the uploads through an HTTP→IPFS gateway.
@@ -24,7 +24,7 @@ Installation
2424
```bash
2525
pip install django-ipfs-storage
2626
```
27-
27+
It uses the only Python maintained library for IPFS (as of March 2023) [IPFS-Toolkit](https://github.com/emendir/IPFS-Toolkit-Python)
2828

2929
Configuration
3030
-------------
@@ -34,11 +34,8 @@ and returns URLs pointing to the public <https://ipfs.io/ipfs/> HTTP Gateway
3434

3535
To customise this, set the following variables in your `settings.py`:
3636

37-
- `IPFS_STORAGE_API_URL`: defaults to `'http://localhost:5001/api/v0/'`.
38-
- `IPFS_GATEWAY_API_URL`: defaults to `'https://ipfs.io/ipfs/'`.
39-
40-
Set `IPFS_GATEWAY_API_URL` to `'http://localhost:8080/ipfs/'` to serve content
41-
through your local daemon's HTTP gateway.
37+
- `IPFS_STORAGE_API_URL`: defaults to `'/ip4/0.0.0.0/tcp/5001'`.
38+
- `IPFS_GATEWAY_API_URL`: defaults to `'/ip4/0.0.0.0/tcp/8008'`.
4239

4340

4441
Usage
@@ -55,9 +52,9 @@ Use IPFS as [Django's default file storage backend](https://docs.djangoproject.c
5552

5653
DEFAULT_FILE_STORAGE = 'ipfs_storage.InterPlanetaryFileSystemStorage'
5754

58-
IPFS_STORAGE_API_URL = 'http://localhost:5001/api/v0/'
59-
IPFS_STORAGE_GATEWAY_URL = 'http://localhost:8080/ipfs/'
60-
```
55+
IPFS_STORAGE_API_URL = '/ip4/localhost/tcp/5001'
56+
IPFS_STORAGE_GATEWAY_URL = '/ip4/localhost/tcp/8008'
57+
```
6158

6259

6360
### For a specific FileField
@@ -67,12 +64,12 @@ Alternatively, you may only want to use the IPFS storage backend for a single fi
6764
```python
6865
from django.db import models
6966

70-
from ipfs_storage import InterPlanetaryFileSystemStorage
67+
from ipfs_storage import InterPlanetaryFileSystemStorage
7168

7269

7370
class MyModel(models.Model):
7471
#
75-
file_stored_on_ipfs = models.FileField(storage=InterPlanetaryFileSystemStorage())
72+
file_stored_on_ipfs = models.FileField(storage=InterPlanetaryFileSystemStorage())
7673
other_file = models.FileField() # will still use DEFAULT_FILE_STORAGE
7774
```
7875

@@ -84,7 +81,7 @@ FAQ
8481

8582
### Why IPFS?
8683

87-
Not my department. See <https://ipfs.io/#why>.
84+
Not my department. See <https://ipfs.io/#why>.
8885

8986
### How do I ensure my uploads are always available?
9087

@@ -99,7 +96,7 @@ See above.
9996
### How do I delete an upload?
10097

10198
Because of the distributed nature of IPFS, anyone who accesses a piece
102-
of content keeps a copy, and reseeds it for you automatically until it's
99+
of content keeps a copy, and reseeds it for you automatically until it's
103100
evicted from their node's local cache. Yay bandwidth costs! Boo censorship!
104101

105102
Unfortunately, if you're trying to censor yourself (often quite necessary),

ipfs_storage/__init__.py

+2-81
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,3 @@
1-
from urllib.parse import urlparse
1+
from .storage import InterPlanetaryFileSystemStorage
22

3-
from django.conf import settings
4-
from django.core.files.base import File, ContentFile
5-
from django.core.files.storage import Storage
6-
from django.utils.deconstruct import deconstructible
7-
import ipfsapi
8-
9-
10-
__version__ = '0.0.4'
11-
12-
13-
@deconstructible
14-
class InterPlanetaryFileSystemStorage(Storage):
15-
"""IPFS Django storage backend.
16-
17-
Only file creation and reading is supported
18-
due to the nature of the IPFS protocol.
19-
"""
20-
21-
def __init__(self, api_url=None, gateway_url=None):
22-
"""Connect to Interplanetary File System daemon API to add/pin files.
23-
24-
:param api_url: IPFS control API base URL.
25-
Also configurable via `settings.IPFS_STORAGE_API_URL`.
26-
Defaults to 'http://localhost:5001/api/v0/'.
27-
:param gateway_url: Base URL for IPFS Gateway (for HTTP-only clients).
28-
Also configurable via `settings.IPFS_STORAGE_GATEWAY_URL`.
29-
Defaults to 'https://ipfs.io/ipfs/'.
30-
"""
31-
parsed_api_url = urlparse(api_url or getattr(settings, 'IPFS_STORAGE_API_URL', 'http://localhost:5001/api/v0/'))
32-
self._ipfs_client = ipfsapi.connect(
33-
parsed_api_url.hostname,
34-
parsed_api_url.port,
35-
parsed_api_url.path.strip('/')
36-
)
37-
self.gateway_url = gateway_url or getattr(settings, 'IPFS_STORAGE_GATEWAY_URL', 'https://ipfs.io/ipfs/')
38-
39-
def _open(self, name: str, mode='rb') -> File:
40-
"""Retrieve the file content identified by multihash.
41-
42-
:param name: IPFS Content ID multihash.
43-
:param mode: Ignored. The returned File instance is read-only.
44-
"""
45-
return ContentFile(self._ipfs_client.cat(name), name=name)
46-
47-
def _save(self, name: str, content: File) -> str:
48-
"""Add and pin content to IPFS daemon.
49-
50-
:param name: Ignored. Provided to comply with `Storage` interface.
51-
:param content: Django File instance to save.
52-
:return: IPFS Content ID multihash.
53-
"""
54-
multihash = self._ipfs_client.add_bytes(content.__iter__())
55-
self._ipfs_client.pin_add(multihash)
56-
return multihash
57-
58-
def get_valid_name(self, name):
59-
"""Returns name. Only provided for compatibility with Storage interface."""
60-
return name
61-
62-
def get_available_name(self, name, max_length=None):
63-
"""Returns name. Only provided for compatibility with Storage interface."""
64-
return name
65-
66-
def size(self, name: str) -> int:
67-
"""Total size, in bytes, of IPFS content with multihash `name`."""
68-
return self._ipfs_client.object_stat(name)['CumulativeSize']
69-
70-
def delete(self, name: str):
71-
"""Unpin IPFS content from the daemon."""
72-
self._ipfs_client.pin_rm(name)
73-
74-
def url(self, name: str):
75-
"""Returns an HTTP-accessible Gateway URL by default.
76-
77-
Override this if you want direct `ipfs://…` URLs or something.
78-
79-
:param name: IPFS Content ID multihash.
80-
:return: HTTP URL to access the content via an IPFS HTTP Gateway.
81-
"""
82-
return '{gateway_url}{multihash}'.format(gateway_url=self.gateway_url, multihash=name)
3+
__version__ = "0.1.0"

ipfs_storage/storage.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from urllib.parse import urlparse
2+
3+
from ipfs_api import ipfshttpclient
4+
5+
from django.conf import settings
6+
from django.core.files.base import File, ContentFile
7+
from django.utils.deconstruct import deconstructible
8+
from django.core.files.storage import Storage
9+
10+
11+
@deconstructible
12+
class InterPlanetaryFileSystemStorage(Storage):
13+
"""IPFS Django storage backend.
14+
15+
Only file creation and reading is supported due to the nature of the IPFS protocol.
16+
"""
17+
18+
def __init__(self, api_url=None, gateway_url=None):
19+
"""Connect to Interplanetary File System daemon API to add/pin files."""
20+
self._ipfs_client = ipfshttpclient.connect(settings.IPFS_STORAGE_API_URL)
21+
self._ipfs_client.config.set(
22+
"Addresses.Gateway", settings.IPFS_STORAGE_GATEWAY_URL
23+
)
24+
25+
def _open(self, name: str, mode="rb") -> File:
26+
"""Retrieve the file content identified by multihash.
27+
28+
:param name: IPFS Content ID multihash.
29+
:param mode: Ignored. The returned File instance is read-only.
30+
"""
31+
return ContentFile(self._ipfs_client.cat(name), name=name)
32+
33+
def _save(self, name: str, content: File) -> str:
34+
"""Add and pin content to IPFS daemon.
35+
36+
:param name: Ignored. Provided to comply with `Storage` interface.
37+
:param content: Django File instance to save.
38+
:return: IPFS Content ID multihash.
39+
"""
40+
multihash = self._ipfs_client.add_bytes(content.__iter__())
41+
self._ipfs_client.pin.add(multihash)
42+
return multihash
43+
44+
def get_valid_name(self, name):
45+
"""Returns name. Only provided for compatibility with Storage interface."""
46+
return name
47+
48+
def get_available_name(self, name, max_length=None):
49+
"""Returns name. Only provided for compatibility with Storage interface."""
50+
return name
51+
52+
def size(self, name: str) -> int:
53+
"""Total size, in bytes, of IPFS content with multihash `name`."""
54+
return self._ipfs_client.object.stat(name)["CumulativeSize"]
55+
56+
def delete(self, name: str):
57+
"""Unpin IPFS content from the daemon."""
58+
self._ipfs_client.pin.rm(name)
59+
60+
def url(self, name: str):
61+
"""Returns an HTTP-accessible Gateway URL by default.
62+
63+
Override this if you want direct `ipfs://…` URLs or something.
64+
65+
:param name: IPFS Content ID multihash.
66+
:return: HTTP URL to access the content via an IPFS HTTP Gateway.
67+
"""
68+
return name

setup.cfg

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[flake8]
2+
ignore = E203, E266, E501
3+
max-line-length = 100
4+
max-complexity = 18
5+
select = B,C,E,F,W,T4,B9
6+
7+
[isort]
8+
use_parentheses = True
9+
multi_line_output = 3
10+
length_sort = 1
11+
lines_between_types = 0
12+
known_django = django
13+
known_third_party = ipfs_api,setuptools
14+
sections = FUTURE, STDLIB, THIRDPARTY, DJANGO, FIRSTPARTY, LOCALFOLDER
15+
no_lines_before = LOCALFOLDER
16+
known_first_party = skatepedia,scraper

setup.py

+19-31
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,32 @@
1+
import os
2+
13
from setuptools import setup, find_packages
2-
from codecs import open
34

45
from ipfs_storage import __version__
56

6-
7-
try:
8-
import pypandoc
9-
long_description = pypandoc.convert('README.md', 'rst')
10-
except(IOError, ImportError):
11-
with open('README.rst', encoding='utf-8') as f:
12-
long_description = f.read()
13-
7+
HERE = os.path.dirname(os.path.abspath(__file__))
8+
README = open(os.path.join(HERE, "README.md")).read()
149

1510
setup(
16-
name='django-ipfs-storage',
17-
description='IPFS storage backend for Django.',
18-
long_description=long_description,
19-
keywords='django ipfs storage',
11+
name="django-ipfs-storage",
12+
description="IPFS storage backend for Django.",
13+
long_description=README,
14+
keywords="django ipfs storage",
2015
version=__version__,
21-
license='MPL 2.0',
22-
23-
author='Ben Jeffrey',
24-
author_email='[email protected]',
25-
url='https://github.com/jeffbr13/django-ipfs-storage',
26-
16+
license="MPL 2.0",
17+
author="Ben Jeffrey",
18+
author_email="[email protected]",
19+
url="https://github.com/skatepedia/django-ipfs-storage",
2720
classifiers=(
28-
'Development Status :: 3 - Alpha',
29-
'Programming Language :: Python :: 3',
30-
'Intended Audience :: Developers',
31-
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
32-
'Framework :: Django',
21+
"Programming Language :: Python :: 3",
22+
"Intended Audience :: Developers",
23+
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
24+
"Framework :: Django",
3325
),
34-
3526
packages=find_packages(),
36-
3727
install_requires=(
38-
'django',
39-
'ipfsapi',
28+
"Django",
29+
"IPFS-Toolkit",
4030
),
41-
setup_requires=(
42-
'pypandoc',
43-
)
31+
test_requires=("pytest"),
4432
)

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.conf import settings
2+
3+
4+
def pytest_configure():
5+
settings.configure(IPFS_STORAGE_API_URL="", IPFS_STORAGE_GATEWAY_URL="")

0 commit comments

Comments
 (0)