Skip to content

Commit 4d3ad82

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 4d3ad82

11 files changed

+199
-134
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

+14-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/8080'`.
4239

4340

4441
Usage
@@ -55,9 +52,10 @@ 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+
57+
IPFS_STORAGE_GATEWAY_URL = '/ip4/localhost/tcp/8080'
58+
```
6159

6260

6361
### For a specific FileField
@@ -67,12 +65,12 @@ Alternatively, you may only want to use the IPFS storage backend for a single fi
6765
```python
6866
from django.db import models
6967

70-
from ipfs_storage import InterPlanetaryFileSystemStorage
68+
from ipfs_storage import InterPlanetaryFileSystemStorage
7169

7270

7371
class MyModel(models.Model):
7472
#
75-
file_stored_on_ipfs = models.FileField(storage=InterPlanetaryFileSystemStorage())
73+
file_stored_on_ipfs = models.FileField(storage=InterPlanetaryFileSystemStorage())
7674
other_file = models.FileField() # will still use DEFAULT_FILE_STORAGE
7775
```
7876

@@ -84,7 +82,7 @@ FAQ
8482

8583
### Why IPFS?
8684

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

8987
### How do I ensure my uploads are always available?
9088

@@ -99,7 +97,7 @@ See above.
9997
### How do I delete an upload?
10098

10199
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
100+
of content keeps a copy, and reseeds it for you automatically until it's
103101
evicted from their node's local cache. Yay bandwidth costs! Boo censorship!
104102

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

ipfs_storage/__init__.py

+1-82
Original file line numberDiff line numberDiff line change
@@ -1,82 +1 @@
1-
from urllib.parse import urlparse
2-
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)
1+
from .storage import InterPlanetaryFileSystemStorage

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

+20-34
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,30 @@
1-
from setuptools import setup, find_packages
2-
from codecs import open
3-
4-
from ipfs_storage import __version__
1+
import os
52

3+
from setuptools import setup, find_packages
64

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-
5+
HERE = os.path.dirname(os.path.abspath(__file__))
6+
README = open(os.path.join(HERE, "README.md")).read()
147

158
setup(
16-
name='django-ipfs-storage',
17-
description='IPFS storage backend for Django.',
18-
long_description=long_description,
19-
keywords='django ipfs storage',
20-
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-
9+
name="django-ipfs-storage",
10+
description="IPFS storage backend for Django.",
11+
long_description=README,
12+
keywords="django ipfs storage",
13+
version="0.1.0",
14+
license="MPL 2.0",
15+
author="Ben Jeffrey",
16+
author_email="[email protected]",
17+
url="https://github.com/skatepedia/django-ipfs-storage",
2718
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',
19+
"Programming Language :: Python :: 3",
20+
"Intended Audience :: Developers",
21+
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
22+
"Framework :: Django",
3323
),
34-
3524
packages=find_packages(),
36-
3725
install_requires=(
38-
'django',
39-
'ipfsapi',
26+
"Django",
27+
"IPFS-Toolkit",
4028
),
41-
setup_requires=(
42-
'pypandoc',
43-
)
29+
test_requires=("pytest"),
4430
)

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)