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
|
||||
`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
|
||||
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`,
|
||||
|
||||
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")]
|
||||
Tokenless,
|
||||
#[error("{0}")]
|
||||
InvalidUpload(&'static str),
|
||||
#[error("{0}")]
|
||||
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);
|
||||
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_FILE_EXISTS: CustomError = CustomError("File already exists, cannot upload", ErrorCode::BadRequest);
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
@ -158,6 +161,7 @@ impl IntoResponse for ApiError {
|
||||
ApiError::DisabledFeature{..} => (StatusCode::NOT_FOUND, ErrorCode::BadRequest),
|
||||
ApiError::Unauthorized{..} => (StatusCode::FORBIDDEN, ErrorCode::BadRequest),
|
||||
ApiError::Tokenless => (StatusCode::FORBIDDEN, ErrorCode::InvalidToken),
|
||||
ApiError::InvalidUpload(_) => (StatusCode::BAD_REQUEST, ErrorCode::BadRequest),
|
||||
ApiError::CustomError(ref e) => (StatusCode::INTERNAL_SERVER_ERROR, e.1),
|
||||
};
|
||||
(status, Json(ErrorResponse{
|
||||
@ -368,7 +372,7 @@ pub async fn create_repo(State(state): State<ServerState<'_>>,
|
||||
}
|
||||
|
||||
// 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 _: 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)
|
||||
.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 _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, token).await?;
|
||||
Ok(())
|
||||
@ -582,11 +586,14 @@ pub async fn delete_entry(State(state): State<ServerState<'_>>,
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
struct UploadFile {
|
||||
name: String,
|
||||
prefix: String,
|
||||
git_ref: String,
|
||||
sha: String,
|
||||
extension: String,
|
||||
data: Bytes,
|
||||
folder: String,
|
||||
is_folder: bool,
|
||||
}
|
||||
|
||||
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() })?;
|
||||
|
||||
// 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? {
|
||||
let name = match field.name() {
|
||||
Some(x) => x,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
match name {
|
||||
"name" => file.name = field.text().await?,
|
||||
"file" => file.data = field.bytes().await?,
|
||||
"folder" => file.folder = field.text().await?,
|
||||
match name { // Verify valid inputs.
|
||||
"ref" => file.git_ref = field.text().await?.trim().to_owned(),
|
||||
"sha" => file.sha = {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
(!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 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::set_permissions(&filepath, Permissions::from_mode(FILE_PERM))?;
|
||||
|
||||
let tardir = dir.join(&file.folder);
|
||||
if file.folder != *"" {
|
||||
if file.is_folder {
|
||||
let tardir = dir.join(&name);
|
||||
if fs::exists(&tardir)? {
|
||||
fs::remove_dir_all(&tardir)?;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user