feat: added file uploading API, Gitea action, and description in README
This commit is contained in:
parent
f94ec335dd
commit
446c4dc851
20
README.md
20
README.md
@ -13,6 +13,25 @@ permissions, and make sure the upload path has permissions `750`. Indeed, all
|
|||||||
subdirectories are created with this permissions, and all files are created with
|
subdirectories are created with this permissions, and all files are created with
|
||||||
`640`.
|
`640`.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
The repository also includes a Gitea action. The arguments are
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
inputs:
|
||||||
|
file: ... # The file to upload.
|
||||||
|
feature: ... # The feature category for pack, i.e docs.
|
||||||
|
secret: ${{ secrets.PackRepo }} # The authorization secret for this repository.
|
||||||
|
prefix: ... # An optional prefix for the filename.
|
||||||
|
is_folder: ... # Specify as 'true' if you are deliberately uploading a folder.
|
||||||
|
```
|
||||||
|
|
||||||
|
Pack chooses to enforce naming schemes depending on the type of trigger,
|
||||||
|
looking at the `gitea.ref` context variable, which is the fully formed
|
||||||
|
reference. In the case this is a git reference,
|
||||||
|
the filename will be `{prefix}-{ref_name}-{sha}`. For actions triggered by
|
||||||
|
releases, the `gitea.ref` is defined as the release tag, and the filename becomes
|
||||||
|
`{prefix}-{release}`.
|
||||||
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
The only API endpoint intended to be exposed is `/api/files/{owner}/{repo}/{*path}`, which
|
The only API endpoint intended to be exposed is `/api/files/{owner}/{repo}/{*path}`, which
|
||||||
@ -36,6 +55,7 @@ is shown below.
|
|||||||
},
|
},
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If the requested path is a file, it will redirect to download the file. If using `curl`,
|
If the requested path is a file, it will redirect to download the file. If using `curl`,
|
||||||
|
|||||||
51
action.yml
Normal file
51
action.yml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
name: 'pack'
|
||||||
|
description: 'Use pack to upload a package'
|
||||||
|
inputs:
|
||||||
|
file:
|
||||||
|
description: 'the file to upload'
|
||||||
|
required: true
|
||||||
|
feature:
|
||||||
|
description: 'the feature to upload to, i.e docs, nightly'
|
||||||
|
required: true
|
||||||
|
secret:
|
||||||
|
description: 'the secret for pack'
|
||||||
|
required: true
|
||||||
|
prefix:
|
||||||
|
description: 'an optional prefix for the filename'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
is_folder:
|
||||||
|
description: 'specify as "true" if uploading a folder'
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- run: if [ ${{ inputs.is_folder }} == true ] && [ ! -d ${{ inputs.file }} ]; then echo "Not a folder!"; exit 1; fi
|
||||||
|
- run: if [ ${{ inputs.is_folder }} == false ] && [ ! -f ${{ inputs.file }} ]; then echo "Not a file!"; exit 1; fi
|
||||||
|
- run: |
|
||||||
|
if [ ${{ inputs.is_folder }} == true ]; then \
|
||||||
|
echo "Packaging folder..." &&
|
||||||
|
cd ${{ inputs.file }} &&
|
||||||
|
tar -czf file.tgz * &&
|
||||||
|
echo "Uploading '${{ inputs.file }}' to '${{ vars.PACK_HOST }}/api/upload/${{ gitea.repository }}/${{ inputs.feature }}'" &&
|
||||||
|
curl -v -H 'Authorization: Bearer ${{ inputs.secret }}' \
|
||||||
|
-F sha=${{ gitea.sha }} \
|
||||||
|
-F ref='${{ gitea.ref }}' \
|
||||||
|
-F prefix='${{ inputs.prefix }}' \
|
||||||
|
-F filename=file.tgz \
|
||||||
|
-F data=@file.tgz \
|
||||||
|
-F is_folder='${{ inputs.is_folder }}' \
|
||||||
|
${{ vars.PACK_HOST }}/api/upload/${{ gitea.repository }}/${{ inputs.feature }}
|
||||||
|
else
|
||||||
|
echo "Uploading '${{ inputs.file }}' to '${{ vars.PACK_HOST }}/api/upload/${{ gitea.repository }}/${{ inputs.feature }}'" &&
|
||||||
|
curl -v -H 'Authorization: Bearer ${{ inputs.secret }}' \
|
||||||
|
-F sha=${{ gitea.sha }} \
|
||||||
|
-F ref='${{ gitea.ref }}' \
|
||||||
|
-F prefix='${{ inputs.prefix }}' \
|
||||||
|
-F filename='${{ inputs.file }}' \
|
||||||
|
-F data=@'${{ inputs.file }}' \
|
||||||
|
-F is_folder='${{ inputs.is_folder }}' \
|
||||||
|
${{ vars.PACK_HOST }}/api/upload/${{ gitea.repository }}/${{ inputs.feature }}
|
||||||
|
fi
|
||||||
|
- run: echo "Upload complete"
|
||||||
80
src/api.rs
80
src/api.rs
@ -119,6 +119,8 @@ pub enum ApiError {
|
|||||||
#[error("A token was not provided or is malformed")]
|
#[error("A token was not provided or is malformed")]
|
||||||
Tokenless,
|
Tokenless,
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
|
InvalidUpload(&'static str),
|
||||||
|
#[error("{0}")]
|
||||||
CustomError(#[from] CustomError)
|
CustomError(#[from] CustomError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +132,7 @@ static ERR_FILE_CALL_UTF: CustomError = CustomError("file command output failed
|
|||||||
static ERR_FILE_CALL_UNEXPECTED: CustomError = CustomError("file command output had unexpected output", ErrorCode::Filesystem);
|
static ERR_FILE_CALL_UNEXPECTED: CustomError = CustomError("file command output had unexpected output", ErrorCode::Filesystem);
|
||||||
pub static ERR_CONTENT_FOLDER_RENDER: CustomError = CustomError("Unable to process 'ContentFolder' template", ErrorCode::Filesystem);
|
pub static ERR_CONTENT_FOLDER_RENDER: CustomError = CustomError("Unable to process 'ContentFolder' template", ErrorCode::Filesystem);
|
||||||
static ERR_IMPOSSIBLE_ENTRY: CustomError = CustomError("Entry is neither file, directory or symlink", ErrorCode::Filesystem);
|
static ERR_IMPOSSIBLE_ENTRY: CustomError = CustomError("Entry is neither file, directory or symlink", ErrorCode::Filesystem);
|
||||||
|
static ERR_FILE_EXISTS: CustomError = CustomError("File already exists, cannot upload", ErrorCode::BadRequest);
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct ErrorResponse {
|
pub struct ErrorResponse {
|
||||||
@ -158,6 +161,7 @@ impl IntoResponse for ApiError {
|
|||||||
ApiError::DisabledFeature{..} => (StatusCode::NOT_FOUND, ErrorCode::BadRequest),
|
ApiError::DisabledFeature{..} => (StatusCode::NOT_FOUND, ErrorCode::BadRequest),
|
||||||
ApiError::Unauthorized{..} => (StatusCode::FORBIDDEN, ErrorCode::BadRequest),
|
ApiError::Unauthorized{..} => (StatusCode::FORBIDDEN, ErrorCode::BadRequest),
|
||||||
ApiError::Tokenless => (StatusCode::FORBIDDEN, ErrorCode::InvalidToken),
|
ApiError::Tokenless => (StatusCode::FORBIDDEN, ErrorCode::InvalidToken),
|
||||||
|
ApiError::InvalidUpload(_) => (StatusCode::BAD_REQUEST, ErrorCode::BadRequest),
|
||||||
ApiError::CustomError(ref e) => (StatusCode::INTERNAL_SERVER_ERROR, e.1),
|
ApiError::CustomError(ref e) => (StatusCode::INTERNAL_SERVER_ERROR, e.1),
|
||||||
};
|
};
|
||||||
(status, Json(ErrorResponse{
|
(status, Json(ErrorResponse{
|
||||||
@ -368,7 +372,7 @@ pub async fn create_repo(State(state): State<ServerState<'_>>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add secret to Gitea secrets.
|
// Add secret to Gitea secrets.
|
||||||
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, &repo);
|
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PackRepo", &state.config.gitea_host, &repo);
|
||||||
let data = GiteaSetSecret { data: secret };
|
let data = GiteaSetSecret { data: secret };
|
||||||
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, &token).await?;
|
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, &token).await?;
|
||||||
|
|
||||||
@ -423,7 +427,7 @@ async fn update_secret(state: &ServerState<'_>, repo: &str, token: &str) -> Resu
|
|||||||
.map(|_| 0)
|
.map(|_| 0)
|
||||||
.sum(); // Evaluate statement.
|
.sum(); // Evaluate statement.
|
||||||
|
|
||||||
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, repo);
|
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PackRepo", &state.config.gitea_host, repo);
|
||||||
let data = GiteaSetSecret { data: secret };
|
let data = GiteaSetSecret { data: secret };
|
||||||
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, token).await?;
|
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, token).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -582,11 +586,14 @@ pub async fn delete_entry(State(state): State<ServerState<'_>>,
|
|||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Debug)]
|
||||||
struct UploadFile {
|
struct UploadFile {
|
||||||
name: String,
|
prefix: String,
|
||||||
|
git_ref: String,
|
||||||
|
sha: String,
|
||||||
|
extension: String,
|
||||||
data: Bytes,
|
data: Bytes,
|
||||||
folder: String,
|
is_folder: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upload(State(state): State<ServerState<'_>>,
|
pub async fn upload(State(state): State<ServerState<'_>>,
|
||||||
@ -615,28 +622,75 @@ pub async fn upload(State(state): State<ServerState<'_>>,
|
|||||||
.ok_or(ApiError::DisabledFeature{ feature: feature.clone() })?;
|
.ok_or(ApiError::DisabledFeature{ feature: feature.clone() })?;
|
||||||
|
|
||||||
// Process multipart.
|
// Process multipart.
|
||||||
let mut file = UploadFile{ folder: "".to_owned(), ..Default::default() };
|
let mut file = UploadFile{ ..Default::default() };
|
||||||
while let Some(field) = multipart.next_field().await? {
|
while let Some(field) = multipart.next_field().await? {
|
||||||
let name = match field.name() {
|
let name = match field.name() {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
match name {
|
match name { // Verify valid inputs.
|
||||||
"name" => file.name = field.text().await?,
|
"ref" => file.git_ref = field.text().await?.trim().to_owned(),
|
||||||
"file" => file.data = field.bytes().await?,
|
"sha" => file.sha = {
|
||||||
"folder" => file.folder = field.text().await?,
|
let sha = field.text().await?.trim().to_owned();
|
||||||
|
(sha.len() >= 10).then_some(0).ok_or(ApiError::InvalidUpload("SHA improperly formatted"))?;
|
||||||
|
u64::from_str_radix(&sha[..10], 16).map_err(|_| ApiError::InvalidUpload("SHA improperly formatted"))?;
|
||||||
|
sha[..10].to_owned()
|
||||||
|
},
|
||||||
|
"prefix" => file.prefix = field.text().await?.trim().to_owned(),
|
||||||
|
"filename" => file.extension = path::Path::new(&field.text().await?.trim()).extension()
|
||||||
|
.map_or_else(|| "", |v| v.to_str().unwrap())
|
||||||
|
.to_owned(),
|
||||||
|
"data" => file.data = field.bytes().await?,
|
||||||
|
"is_folder" => file.is_folder = field.text().await?.trim() == "true",
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(!file.git_ref.is_empty()).then_some(0).ok_or(ApiError::InvalidUpload("Missing ref"))?;
|
||||||
|
(!file.sha.is_empty()).then_some(0).ok_or(ApiError::InvalidUpload("Missing SHA"))?;
|
||||||
|
|
||||||
|
let mut is_release = false;
|
||||||
|
let ref_name = if file.git_ref.starts_with("refs/heads/") { // Branches
|
||||||
|
file.git_ref.replace("refs/heads/", "").replace("/", "_")
|
||||||
|
} else if file.git_ref.starts_with("refs/tags/") { // Tags
|
||||||
|
file.git_ref.replace("refs/tags/", "").replace("/", "_")
|
||||||
|
} else if file.git_ref.starts_with("refs/") { // Other
|
||||||
|
return Err(ApiError::InvalidUpload("Cannot handle files uploaded through this action trigger"));
|
||||||
|
} else { // Release
|
||||||
|
is_release = true;
|
||||||
|
file.git_ref
|
||||||
|
};
|
||||||
|
|
||||||
|
// Name and folder processing
|
||||||
|
let mut name = match is_release {
|
||||||
|
true => ref_name,
|
||||||
|
false => format!("{}-{}", ref_name, file.sha)
|
||||||
|
};
|
||||||
|
if !file.prefix.is_empty() {
|
||||||
|
name = format!("{}-{}", file.prefix, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_ext = if !file.extension.is_empty() {
|
||||||
|
format!("{}.{}", name, file.extension)
|
||||||
|
} else {
|
||||||
|
(!file.is_folder)
|
||||||
|
.then_some(0)
|
||||||
|
.ok_or(ApiError::InvalidUpload("Uploading folder should be zipped"))?; // Should not happen if uploading a folder.
|
||||||
|
name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let dir = path::Path::new(&state.config.upload_path).join(&repo).join(feature);
|
let dir = path::Path::new(&state.config.upload_path).join(&repo).join(feature);
|
||||||
let filepath = dir.join(&file.name);
|
let filepath = dir.join(&name_ext);
|
||||||
|
if fs::exists(&filepath)? { // File should not exist.
|
||||||
|
return Err(ERR_FILE_EXISTS)?;
|
||||||
|
}
|
||||||
|
|
||||||
fs::write(&filepath, file.data)?;
|
fs::write(&filepath, file.data)?;
|
||||||
fs::set_permissions(&filepath, Permissions::from_mode(FILE_PERM))?;
|
fs::set_permissions(&filepath, Permissions::from_mode(FILE_PERM))?;
|
||||||
|
|
||||||
let tardir = dir.join(&file.folder);
|
if file.is_folder {
|
||||||
if file.folder != *"" {
|
let tardir = dir.join(&name);
|
||||||
if fs::exists(&tardir)? {
|
if fs::exists(&tardir)? {
|
||||||
fs::remove_dir_all(&tardir)?;
|
fs::remove_dir_all(&tardir)?;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user