mirror of
https://github.com/openvk/openvk
synced 2024-12-22 08:31:18 +03:00
Add photos.save, photos.saveWallPhoto, photos.saveOwnerPhoto, photos.getUploadServer
Didn't test, but shouldn't be really tough to fix afterwards if it'll break.
This commit is contained in:
parent
f7a2da2cbf
commit
1a2a0805d4
10 changed files with 367 additions and 21 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -5,7 +5,7 @@ update.pid.old
|
||||||
Web/static/js/node_modules
|
Web/static/js/node_modules
|
||||||
|
|
||||||
tmp/*
|
tmp/*
|
||||||
!tmp/.gitkeep
|
!tmp/api-storage
|
||||||
!tmp/themepack_artifacts/.gitkeep
|
!tmp/themepack_artifacts/.gitkeep
|
||||||
themepacks/*
|
themepacks/*
|
||||||
!themepacks/.gitkeep
|
!themepacks/.gitkeep
|
||||||
|
|
230
VKAPI/Handlers/Photos.php
Normal file
230
VKAPI/Handlers/Photos.php
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\VKAPI\Handlers;
|
||||||
|
|
||||||
|
use Nette\InvalidStateException;
|
||||||
|
use Nette\Utils\ImageException;
|
||||||
|
use openvk\Web\Models\Entities\Photo;
|
||||||
|
use openvk\Web\Models\Repositories\Albums;
|
||||||
|
use openvk\Web\Models\Repositories\Clubs;
|
||||||
|
|
||||||
|
final class Photos extends VKAPIRequestHandler
|
||||||
|
{
|
||||||
|
private function getPhotoUploadUrl(string $field, int $group = 0, bool $multifile = false): string
|
||||||
|
{
|
||||||
|
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
|
||||||
|
$uploadInfo = [
|
||||||
|
1,
|
||||||
|
$field,
|
||||||
|
(int) $multifile,
|
||||||
|
0,
|
||||||
|
time(),
|
||||||
|
$this->getUser()->getId(),
|
||||||
|
$group,
|
||||||
|
0, # this is unused but stays here base64 reasons (X2 doesn't work, so there's dummy value for short)
|
||||||
|
];
|
||||||
|
$uploadInfo = pack("vZ10v2P3S", ...$uploadInfo);
|
||||||
|
$uploadInfo = base64_encode($uploadInfo);
|
||||||
|
$uploadHash = hash_hmac("sha3-224", $uploadInfo, $secret);
|
||||||
|
$uploadInfo = rawurlencode($uploadInfo);
|
||||||
|
|
||||||
|
return ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/upload/photo/$uploadHash?$uploadInfo";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getImagePath(string $photo, string $hash, ?string& $up = NULL, ?string& $group = NULL): string
|
||||||
|
{
|
||||||
|
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
|
||||||
|
if(!hash_equals(hash_hmac("sha3-224", $photo, $secret), $hash))
|
||||||
|
$this->fail(121, "Incorrect hash");
|
||||||
|
|
||||||
|
[$up, $image, $group] = explode("|", $photo);
|
||||||
|
|
||||||
|
$imagePath = __DIR__ . "/../../tmp/api-storage/photos/$up" . "_$image.oct";
|
||||||
|
if(!file_exists($imagePath))
|
||||||
|
$this->fail(10, "Invalid image");
|
||||||
|
|
||||||
|
return $imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnerPhotoUploadServer(int $owner_id = 0): object
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
if($owner_id < 0) {
|
||||||
|
$club = (new Clubs)->get(abs($owner_id));
|
||||||
|
if(!$club)
|
||||||
|
$this->fail(0404, "Club not found");
|
||||||
|
else if(!$club->canBeModifiedBy($this->getUser()))
|
||||||
|
$this->fail(200, "Access: Club can't be 'written' by user");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"upload_url" => $this->getPhotoUploadUrl("photo", isset($club) ? 0 : $club->getId()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveOwnerPhoto(string $photo, string $hash): object
|
||||||
|
{
|
||||||
|
$imagePath = $this->getImagePath($photo, $hash, $uploader, $group);
|
||||||
|
if($group == 0) {
|
||||||
|
$user = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader);
|
||||||
|
$album = (new Albums)->getUserAvatarAlbum($user);
|
||||||
|
} else {
|
||||||
|
$club = (new Clubs)->get((int) $group);
|
||||||
|
$album = (new Albums)->getClubAvatarAlbum($club);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$avatar = new Photo;
|
||||||
|
$avatar->setOwner((int) $uploader);
|
||||||
|
$avatar->setDescription("Profile photo");
|
||||||
|
$avatar->setCreated(time());
|
||||||
|
$avatar->setFile([
|
||||||
|
"tmp_name" => $imagePath,
|
||||||
|
"error" => 0,
|
||||||
|
]);
|
||||||
|
$avatar->save();
|
||||||
|
$album->addPhoto($avatar);
|
||||||
|
unlink($imagePath);
|
||||||
|
} catch(ImageException | InvalidStateException $e) {
|
||||||
|
unlink($imagePath);
|
||||||
|
$this->fail(129, "Invalid image file");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"photo_hash" => NULL,
|
||||||
|
"photo_src" => $avatar->getURL(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWallUploadServer(?int $group_id = NULL): object
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
$album = NULL;
|
||||||
|
if(!is_null($group_id)) {
|
||||||
|
$club = (new Clubs)->get(abs($group_id));
|
||||||
|
if(!$club)
|
||||||
|
$this->fail(0404, "Club not found");
|
||||||
|
else if(!$club->canBeModifiedBy($this->getUser()))
|
||||||
|
$this->fail(200, "Access: Club can't be 'written' by user");
|
||||||
|
} else {
|
||||||
|
$album = (new Albums)->getUserWallAlbum($this->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"upload_url" => $this->getPhotoUploadUrl("photo", $group_id ?? 0),
|
||||||
|
"album_id" => $album,
|
||||||
|
"user_id" => $this->getUser()->getId(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveWallPhoto(string $photo, string $hash, int $group_id = 0, ?string $caption = NULL): array
|
||||||
|
{
|
||||||
|
$imagePath = $this->getImagePath($photo, $hash, $uploader, $group);
|
||||||
|
if($group_id != $group)
|
||||||
|
$this->fail(8, "group_id doesn't match");
|
||||||
|
|
||||||
|
$album = NULL;
|
||||||
|
if($group_id != 0) {
|
||||||
|
$uploader = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader);
|
||||||
|
$album = (new Albums)->getUserWallAlbum($uploader);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$photo = new Photo;
|
||||||
|
$photo->setOwner((int) $uploader);
|
||||||
|
$photo->setCreated(time());
|
||||||
|
$photo->setFile([
|
||||||
|
"tmp_name" => $imagePath,
|
||||||
|
"error" => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!is_null($caption))
|
||||||
|
$photo->setDescription($caption);
|
||||||
|
|
||||||
|
$photo->save();
|
||||||
|
unlink($imagePath);
|
||||||
|
} catch(ImageException | InvalidStateException $e) {
|
||||||
|
unlink($imagePath);
|
||||||
|
$this->fail(129, "Invalid image file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_null($album))
|
||||||
|
$album->addPhoto($photo);
|
||||||
|
|
||||||
|
return [
|
||||||
|
$photo->toVkApiStruct(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploadServer(?int $album_id = NULL): object
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
# Not checking rights to album because save() method will do so anyways
|
||||||
|
return (object) [
|
||||||
|
"upload_url" => $this->getPhotoUploadUrl("photo", 0, true),
|
||||||
|
"album_id" => $album_id,
|
||||||
|
"user_id" => $this->getUser()->getId(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(string $photos_list, string $hash, int $album_id = 0, ?string $caption = NULL): object
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
|
||||||
|
if(!hash_equals(hash_hmac("sha3-224", $photos_list, $secret), $hash))
|
||||||
|
$this->fail(121, "Incorrect hash");
|
||||||
|
|
||||||
|
$album = NULL;
|
||||||
|
if($album_id != 0) {
|
||||||
|
$album_ = (new Albums)->get($album_id);
|
||||||
|
if(!$album_)
|
||||||
|
$this->fail(0404, "Invalid album");
|
||||||
|
else if(!$album_->canBeModifiedBy($this->getUser()))
|
||||||
|
$this->fail(15, "Access: Album can't be 'written' by user");
|
||||||
|
|
||||||
|
$album = $album_;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pList = json_decode($photos_list);
|
||||||
|
$imagePaths = [];
|
||||||
|
foreach($pList as $pDesc)
|
||||||
|
$imagePaths[] = __DIR__ . "/../../tmp/api-storage/photos/$pDesc->keyholder" . "_$pDesc->resource.oct";
|
||||||
|
|
||||||
|
$images = [];
|
||||||
|
try {
|
||||||
|
foreach($imagePaths as $imagePath) {
|
||||||
|
$photo = new Photo;
|
||||||
|
$photo->setOwner($this->getUser()->getId());
|
||||||
|
$photo->setCreated(time());
|
||||||
|
$photo->setFile([
|
||||||
|
"tmp_name" => $imagePath,
|
||||||
|
"error" => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!is_null($caption))
|
||||||
|
$photo->setDescription($caption);
|
||||||
|
|
||||||
|
$photo->save();
|
||||||
|
unlink($imagePath);
|
||||||
|
|
||||||
|
if(!is_null($album))
|
||||||
|
$album->addPhoto($photo);
|
||||||
|
|
||||||
|
$images[] = $photo->toVkApiStruct();
|
||||||
|
}
|
||||||
|
} catch(ImageException | InvalidStateException $e) {
|
||||||
|
foreach($imagePaths as $imagePath)
|
||||||
|
unlink($imagePath);
|
||||||
|
|
||||||
|
$this->fail(129, "Invalid image file");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"count" => sizeof($images),
|
||||||
|
"items" => $images,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ class Photo extends Media
|
||||||
protected $fileExtension = "jpeg";
|
protected $fileExtension = "jpeg";
|
||||||
|
|
||||||
const ALLOWED_SIDE_MULTIPLIER = 7;
|
const ALLOWED_SIDE_MULTIPLIER = 7;
|
||||||
|
|
||||||
protected function saveFile(string $filename, string $hash): bool
|
protected function saveFile(string $filename, string $hash): bool
|
||||||
{
|
{
|
||||||
$image = Image::fromFile($filename);
|
$image = Image::fromFile($filename);
|
||||||
|
@ -24,7 +24,7 @@ class Photo extends Media
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function crop(real $left, real $top, real $width, real $height): bool
|
function crop(real $left, real $top, real $width, real $height): void
|
||||||
{
|
{
|
||||||
if(isset($this->changes["hash"]))
|
if(isset($this->changes["hash"]))
|
||||||
$hash = $this->changes["hash"];
|
$hash = $this->changes["hash"];
|
||||||
|
@ -35,7 +35,7 @@ class Photo extends Media
|
||||||
|
|
||||||
$image = Image::fromFile($this->pathFromHash($hash));
|
$image = Image::fromFile($this->pathFromHash($hash));
|
||||||
$image->crop($left, $top, $width, $height);
|
$image->crop($left, $top, $width, $height);
|
||||||
return $image->save($this->pathFromHash($hash));
|
$image->save($this->pathFromHash($hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isolate(): void
|
function isolate(): void
|
||||||
|
@ -45,8 +45,46 @@ class Photo extends Media
|
||||||
|
|
||||||
DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete();
|
DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo
|
function getDimensions(): array
|
||||||
|
{
|
||||||
|
$hash = $this->getRecord()->hash;
|
||||||
|
|
||||||
|
return array_slice(getimagesize($this->pathFromHash($hash)), 0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDimentions(): array
|
||||||
|
{
|
||||||
|
trigger_error("getDimentions is deprecated, use Photo::getDimensions instead.");
|
||||||
|
|
||||||
|
return $this->getDimensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlbum(): ?Album
|
||||||
|
{
|
||||||
|
return (new Albums)->getAlbumByPhotoId($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toVkApiStruct(): object
|
||||||
|
{
|
||||||
|
$res = (object) [];
|
||||||
|
|
||||||
|
$res->id = $res->pid = $this->getId();
|
||||||
|
$res->owner_id = $res->user_id = $this->getOwner()->getId()->getId();
|
||||||
|
$res->aid = $res->album_id = NULL;
|
||||||
|
$res->width = $this->getDimensions()[0];
|
||||||
|
$res->height = $this->getDimensions()[1];
|
||||||
|
$res->date = $res->created = $this->getPublicationTime()->timestamp();
|
||||||
|
|
||||||
|
$res->src =
|
||||||
|
$res->src_small = $res->src_big = $res->src_xbig = $res->src_xxbig =
|
||||||
|
$res->src_xxxbig = $res->photo_75 = $res->photo_130 = $res->photo_604 =
|
||||||
|
$res->photo_807 = $res->photo_1280 = $res->photo_2560 = $this->getURL();
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function fastMake(int $owner, array $file, string $description = "", ?Album $album = NULL, bool $anon = false): Photo
|
||||||
{
|
{
|
||||||
$photo = new static;
|
$photo = new static;
|
||||||
$photo->setOwner($owner);
|
$photo->setOwner($owner);
|
||||||
|
@ -55,22 +93,10 @@ class Photo extends Media
|
||||||
$photo->setCreated(time());
|
$photo->setCreated(time());
|
||||||
$photo->setFile($file);
|
$photo->setFile($file);
|
||||||
$photo->save();
|
$photo->save();
|
||||||
|
|
||||||
if(!is_null($album))
|
if(!is_null($album))
|
||||||
$album->addPhoto($photo);
|
$album->addPhoto($photo);
|
||||||
|
|
||||||
return $photo;
|
return $photo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDimentions()
|
|
||||||
{
|
|
||||||
$hash = $this->getRecord()->hash;
|
|
||||||
|
|
||||||
return getimagesize($this->pathFromHash($hash));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlbum(): ?Album
|
|
||||||
{
|
|
||||||
return (new Albums)->getAlbumByPhotoId($this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,7 +285,6 @@ final class UserPresenter extends OpenVKPresenter
|
||||||
$photo->setCreated(time());
|
$photo->setCreated(time());
|
||||||
$photo->save();
|
$photo->save();
|
||||||
} catch(ISE $ex) {
|
} catch(ISE $ex) {
|
||||||
$name = $album->getName();
|
|
||||||
$this->flashFail("err", tr("error"), tr("error_upload_failed"));
|
$this->flashFail("err", tr("error"), tr("error_upload_failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,92 @@ final class VKAPIPresenter extends OpenVKPresenter
|
||||||
exit; # Terminate request processing as this is definitely a CORS preflight request.
|
exit; # Terminate request processing as this is definitely a CORS preflight request.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPhotoUpload(string $signature): void
|
||||||
|
{
|
||||||
|
$secret = CHANDLER_ROOT_CONF["security"]["secret"];
|
||||||
|
$computedSignature = hash_hmac("sha3-224", $_SERVER["QUERY_STRING"], $secret);
|
||||||
|
if(!(strlen($signature) == 56 && sodium_memcmp($signature, $computedSignature) == 0)) {
|
||||||
|
header("HTTP/1.1 422 Unprocessable Entity");
|
||||||
|
exit("Try harder <3");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = unpack("vDOMAIN/Z10FIELD/vMF/vMP/PTIME/PUSER/PGROUP", base64_decode($_SERVER["QUERY_STRING"]));
|
||||||
|
if((time() - $data["TIME"]) > 600) {
|
||||||
|
header("HTTP/1.1 422 Unprocessable Entity");
|
||||||
|
exit("Expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
$folder = __DIR__ . "../../tmp/api-storage/photos";
|
||||||
|
$maxSize = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFileSize"];
|
||||||
|
$maxFiles = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFilesPerDomain"];
|
||||||
|
$usrFiles = sizeof(glob("$folder/$data[USER]_*.oct"));
|
||||||
|
if($usrFiles >= $maxFiles) {
|
||||||
|
header("HTTP/1.1 507 Insufficient Storage");
|
||||||
|
exit("There are $maxFiles pending already. Please save them before uploading more :3");
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not multifile
|
||||||
|
if($data["MF"] === 0) {
|
||||||
|
$file = $_FILES[$data["FIELD"]];
|
||||||
|
if(!$file) {
|
||||||
|
header("HTTP/1.0 400");
|
||||||
|
exit("No file");
|
||||||
|
} else if($file["error"] != UPLOAD_ERR_OK) {
|
||||||
|
header("HTTP/1.0 500");
|
||||||
|
exit("File could not be consumed");
|
||||||
|
} else if($file["size"] > $maxSize) {
|
||||||
|
header("HTTP/1.0 507 Insufficient Storage");
|
||||||
|
exit("File is too big");
|
||||||
|
}
|
||||||
|
|
||||||
|
move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_" . ($usrFiles + 1) . ".oct");
|
||||||
|
header("HTTP/1.0 202 Accepted");
|
||||||
|
|
||||||
|
$photo = $data["USER"] . "|" . ($usrFiles + 1) . "|" . $data["GROUP"];
|
||||||
|
exit(json_encode([
|
||||||
|
"server" => "ephemeral",
|
||||||
|
"photo" => $photo,
|
||||||
|
"hash" => hash_hmac("sha3-224", $photo, $secret),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = [];
|
||||||
|
for($i = 1; $i <= 5; $i++) {
|
||||||
|
$file = $_FILES[$data["FIELD"] . $i] ?? NULL;
|
||||||
|
if (!$file || $file["error"] != UPLOAD_ERR_OK || $file["size"] > $maxSize) {
|
||||||
|
continue;
|
||||||
|
} else if((sizeof($files) + $usrFiles) > $maxFiles) {
|
||||||
|
# Clear uploaded files since they can't be saved anyway
|
||||||
|
foreach($files as $f)
|
||||||
|
unlink($f);
|
||||||
|
|
||||||
|
header("HTTP/1.1 507 Insufficient Storage");
|
||||||
|
exit("There are $maxFiles pending already. Please save them before uploading more :3");
|
||||||
|
}
|
||||||
|
|
||||||
|
$files[++$usrFiles] = move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_$usrFiles.oct");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sizeof($files) === 0) {
|
||||||
|
header("HTTP/1.0 400");
|
||||||
|
exit("No file");
|
||||||
|
}
|
||||||
|
|
||||||
|
$filesManifest = [];
|
||||||
|
foreach($files as $id => $file)
|
||||||
|
$filesManifest[] = ["keyholder" => $data["USER"], "resource" => $id, "club" => $data["GROUP"]];
|
||||||
|
|
||||||
|
$filesManifest = json_encode($filesManifest);
|
||||||
|
$manifestHash = hash_hmac("sha3-224", $filesManifest, $secret);
|
||||||
|
header("HTTP/1.0 202 Accepted");
|
||||||
|
exit(json_encode([
|
||||||
|
"server" => "ephemeral",
|
||||||
|
"photos_list" => $filesManifest,
|
||||||
|
"album_id" => "undefined",
|
||||||
|
"hash" => $manifestHash,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
function renderRoute(string $object, string $method): void
|
function renderRoute(string $object, string $method): void
|
||||||
{
|
{
|
||||||
|
|
|
@ -279,6 +279,8 @@ routes:
|
||||||
handler: "Support->quickBanInSupport"
|
handler: "Support->quickBanInSupport"
|
||||||
- url: "/admin/support/unban/{num}"
|
- url: "/admin/support/unban/{num}"
|
||||||
handler: "Support->quickUnbanInSupport"
|
handler: "Support->quickUnbanInSupport"
|
||||||
|
- url: "/upload/photo/{text}"
|
||||||
|
handler: "VKAPI->photoUpload"
|
||||||
- url: "/method/{text}.{text}"
|
- url: "/method/{text}.{text}"
|
||||||
handler: "VKAPI->route"
|
handler: "VKAPI->route"
|
||||||
- url: "/token"
|
- url: "/token"
|
||||||
|
|
|
@ -9,6 +9,9 @@ openvk:
|
||||||
uploads:
|
uploads:
|
||||||
disableLargeUploads: false
|
disableLargeUploads: false
|
||||||
mode: "basic"
|
mode: "basic"
|
||||||
|
api:
|
||||||
|
maxFilesPerDomain: 10
|
||||||
|
maxFileSize: 25000000
|
||||||
shortcodes:
|
shortcodes:
|
||||||
minLength: 3 # won't affect existing short urls or the ones set via admin panel
|
minLength: 3 # won't affect existing short urls or the ones set via admin panel
|
||||||
forbiddenNames:
|
forbiddenNames:
|
||||||
|
|
0
tmp/api-storage/photos/.gitkeep
Normal file
0
tmp/api-storage/photos/.gitkeep
Normal file
0
tmp/api-storage/videos/.gitkeep
Normal file
0
tmp/api-storage/videos/.gitkeep
Normal file
Loading…
Reference in a new issue