Add API to delete releases

This commit is contained in:
rubenwardy 2021-02-02 17:09:28 +00:00
parent e1fe63ab19
commit 912ebbc409
4 changed files with 80 additions and 23 deletions

@ -83,9 +83,6 @@ def homepage():
downloads_result = db.session.query(func.sum(Package.downloads)).one_or_none()
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[0]
tags = db.session.query(func.count(Tags.c.tag_id), Tag) \
.select_from(Tag).outerjoin(Tags).group_by(Tag.id).order_by(db.asc(Tag.title)).all()
def mapPackages(packages):
return [pkg.getAsDictionaryKey() for pkg in packages]
@ -97,7 +94,7 @@ def homepage():
"pop_mod": mapPackages(pop_mod),
"pop_txp": mapPackages(pop_txp),
"pop_game": mapPackages(pop_gam),
"high_reviewed": mapPackages(high_reviewed),
"high_reviewed": mapPackages(high_reviewed)
}
@ -113,9 +110,6 @@ def resolve_package_deps(out, package, only_hard):
if only_hard and dep.optional:
continue
name = None
fulfilled_by = None
if dep.package:
name = dep.package.name
fulfilled_by = [ dep.package.getId() ]
@ -174,7 +168,7 @@ def topic_set_discard():
@bp.route("/api/minetest_versions/")
def versions():
return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
return jsonify([rel.getAsDictionary() \
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
@ -196,8 +190,7 @@ def markdown():
@bp.route("/api/packages/<author>/<name>/releases/")
@is_package_page
def list_releases(package):
releases = package.releases.filter_by(approved=True).all()
return jsonify([ rel.getAsDictionary() for rel in releases ])
return jsonify([ rel.getAsDictionary() for rel in package.releases.all() ])
@bp.route("/api/packages/<author>/<name>/releases/new/", methods=["POST"])
@ -215,14 +208,11 @@ def create_release(token, package):
if "title" not in data:
error(400, "Title is required in the POST data")
if request.json:
if data.get("method") == "git":
for option in ["method", "ref"]:
if option not in data:
error(400, option + " is required in the POST data")
if data["method"].lower() != "git":
error(400, "Release-creation methods other than git are not supported")
return api_create_vcs_release(token, package, data["title"], data["ref"])
elif request.files:
@ -232,6 +222,43 @@ def create_release(token, package):
return api_create_zip_release(token, package, data["title"], file)
else:
error(400, "Unknown release-creation method. Specify the method or provide a file.")
@bp.route("/api/packages/<author>/<name>/releases/<int:id>/")
@is_package_page
def release(package: Package, id: int):
release = PackageRelease.query.get(id)
if release is None or release.package != package:
error(404, "Release not found")
return jsonify(release.getAsDictionary())
@bp.route("/api/packages/<author>/<name>/releases/<int:id>/", methods=["DELETE"])
@csrf.exempt
@is_package_page
@is_api_authd
def delete_release(token: APIToken, package: Package, id: int):
release = PackageRelease.query.get(id)
if release is None or release.package != package:
error(404, "Release not found")
if not token:
error(401, "Authentication needed")
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
if not release.checkPerm(token.owner, Permission.DELETE_RELEASE):
error(403, "Unable to delete the release, make sure there's a newer release available")
db.session.delete(release)
db.session.commit()
return jsonify({"success": True})
@bp.route("/api/packages/<author>/<name>/screenshots/")
@is_package_page

@ -15,7 +15,7 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
### Misc
* GET `/api/whoami/` - Json dictionary with the following keys:
* GET `/api/whoami/` - JSON dictionary with the following keys:
* `is_authenticated` - True on successful API authentication
* `username` - Username of the user authenticated as, null otherwise.
* 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors.
@ -40,13 +40,24 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* `pop_txp` - popular textures
* `pop_game` - popular games
* `high_reviewed` - highest reviewed
* `tags`
### Releases
* GET `/api/packages/<username>/<name>/releases/` (List)
* Returns array of release dictionaries with keys:
* `id`: release ID
* `title`: human-readable title
* `release_date`: Date released
* `url`: download URL
* `commit`: commit hash or null
* `downloads`: number of downloads
* `min_minetest_version`: dict or null, minimum supported minetest version (inclusive).
* `max_minetest_version`: dict or null, minimum supported minetest version (inclusive).
* GET `/api/packages/<username>/<name>/releases/<id>/` (Read)
* POST `/api/packages/<username>/<name>/releases/new/` (Create)
* Requires authentication.
* Body is multipart form if zip upload, JSON otherwise.
* Body can be JSON or multipart form data. Zip uploads must be multipart form data.
* `title`: human-readable name of the release.
* For Git release creation:
* `method`: must be `git`.
@ -54,6 +65,9 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* For zip upload release creation:
* `file`: multipart file to upload, like `<input type=file>`.
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
* DELETE `/api/packages/<username>/<name>/releases/<id>/` (Delete)
* Requires authentication.
* Deletes release.
Examples:
@ -64,9 +78,13 @@ curl -X POST https://content.minetest.net/api/packages/username/name/releases/ne
-d "{\"method\": \"git\", \"title\": \"My Release\", \"ref\": \"master\" }"
# Create release from zip upload
curl https://content.minetest.net/api/packages/username/name/releases/new/ \
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/file.zip
# Delete release
curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/3/ \
-H "Authorization: Bearer YOURTOKEN"
```
### Screenshots
@ -96,7 +114,7 @@ Examples:
```bash
# Create screenshots
curl https://content.minetest.net/api/packages/username/name/screenshots/new/ \
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png

@ -1,4 +1,4 @@
from werkzeug.exceptions import abort
import datetime
from app.logic.LogicError import LogicError
from app.logic.uploads import upload_file
@ -7,10 +7,15 @@ from app.utils import addNotification
def do_create_screenshot(user: User, package: Package, title: str, file):
thirty_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=30)
count = package.screenshots.filter(PackageScreenshot.created_at > thirty_minutes_ago).count()
if count >= 20:
raise LogicError(429, "Too many requests, please wait before trying again")
uploaded_url, uploaded_path = upload_file(file, "image", "a PNG or JPG image file")
counter = 1
for screenshot in package.screenshots:
for screenshot in package.screenshots.all():
screenshot.order = counter
counter += 1

@ -759,6 +759,13 @@ class MinetestRelease(db.Model):
def getActual(self):
return None if self.name == "None" else self
def getAsDictionary(self):
return {
"name": self.name,
"protocol_version": self.protocol,
"is_dev": "-dev" in self.name,
}
@classmethod
def get(cls, version, protocol_num):
if version:
@ -810,8 +817,8 @@ class PackageRelease(db.Model):
"release_date": self.releaseDate.isoformat(),
"commit": self.commit_hash,
"downloads": self.downloads,
"min_protocol": self.min_rel and self.min_rel.protocol,
"max_protocol": self.max_rel and self.max_rel.protocol
"min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(),
"max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(),
}
def getEditURL(self):