From 36afb883ff5d517b3a03e9b887e32f9d031ada40 Mon Sep 17 00:00:00 2001 From: g0t mi1k Date: Mon, 2 Jan 2023 07:12:32 +0000 Subject: [PATCH 1/2] Add PUT multipart support edit_project Using GitLab's API v4, it is possible to change a project's avatar. However the request needs to be submitted via PUT with `Content-Type: multipart/form-data` header set. REF: https://docs.gitlab.com/ee/api/projects.html#upload-a-project-avatar --- lib/GitLab/API/v4.pm | 14 +++++++--- lib/GitLab/API/v4/RESTClient.pm | 48 ++++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/lib/GitLab/API/v4.pm b/lib/GitLab/API/v4.pm index 87ba571..9d960c9 100644 --- a/lib/GitLab/API/v4.pm +++ b/lib/GitLab/API/v4.pm @@ -7321,6 +7321,7 @@ sub create_project_for_user { $api->edit_project( $project_id, \%params, + \%params_multipart, ); Sends a C request to C. @@ -7329,14 +7330,21 @@ Sends a C request to C. sub edit_project { my $self = shift; - croak 'edit_project must be called with 1 to 2 arguments' if @_ < 1 or @_ > 2; + croak 'edit_project must be called with 1 to 3 arguments' if @_ < 1 or @_ > 3; croak 'The #1 argument ($project_id) to edit_project must be a scalar' if ref($_[0]) or (!defined $_[0]); - croak 'The last argument (\%params) to edit_project must be a hash ref' if defined($_[1]) and ref($_[1]) ne 'HASH'; + croak 'The #2 argument (\%params) to edit_project must be a hash ref' if defined($_[1]) and ref($_[1]) ne 'HASH'; + croak 'The last argument (\%params_multipart) to edit_project must be a hash ref' if defined($_[2]) and ref($_[2]) ne 'HASH'; + my $params_multipart = (@_ == 3) ? pop() : undef; my $params = (@_ == 2) ? pop() : undef; my $options = {}; $options->{decode} = 0; $options->{content} = $params if defined $params; - $self->_call_rest_client( 'PUT', 'projects/:project_id', [@_], $options ); + $self->_call_rest_client( 'PUT', 'projects/:project_id', [$_[0]], $options ); + + if (keys %$params_multipart) { + $options->{content}->{file} = $params_multipart ; + $self->_call_rest_client( 'PUT', 'projects/:project_id', [$_[0]], $options ); + } return; } diff --git a/lib/GitLab/API/v4/RESTClient.pm b/lib/GitLab/API/v4/RESTClient.pm index 471dae5..045663b 100644 --- a/lib/GitLab/API/v4/RESTClient.pm +++ b/lib/GitLab/API/v4/RESTClient.pm @@ -162,10 +162,18 @@ sub request { my $req_method = 'request'; my $req = [ $verb, $url, $options ]; + my $boundary; - if ($verb eq 'POST' and ref($content) eq 'HASH' and $content->{file}) { + if (($verb eq 'POST' or $verb eq 'PUT' ) and ref($content) eq 'HASH' and $content->{file}) { $content = { %$content }; - my $file = path( delete $content->{file} ); + my $file = delete $content->{file}; + + my $key = (keys %$file)[0] + if (ref $file); + + $file = (ref $file) + ? path( $file->{$key} ) + : path( $file ); unless (-f $file and -r $file) { local $Carp::Internal{ 'GitLab::API::v4' } = 1; @@ -183,18 +191,34 @@ sub request { }, }; - $req->[0] = $req->[1]; # Replace method with url. - $req->[1] = $data; # Put data where url was. - # So, req went from [$verb,$url,$options] to [$url,$data,$options], - # per the post_multipart interface. - - $req_method = 'post_multipart'; - $content = undef if ! %$content; + if ($verb eq 'POST') { + $req->[0] = $req->[1]; # Replace method with url. + $req->[1] = $data; # Put data where url was. + # So, req went from [$verb,$url,$options] to [$url,$data,$options], + # per the post_multipart interface. + + $req_method = 'post_multipart'; + $content = undef if ! %$content; + } elsif ($verb eq 'PUT') { + $boundary .= sprintf("%x", rand 16) for 1..16; + $content = <<"EOL"; +--------------------------$boundary +Content-Disposition: form-data; name="$key"; filename="$data->{file}->{filename}" + +$data->{file}->{content} +--------------------------$boundary-- +EOL + } } - if (ref $content) { - $content = $self->json->encode( $content ); - $headers->{'content-type'} = 'application/json'; + if (defined $boundary or ref $content) { + $content = $self->json->encode( $content ) + if (ref $content); + + $headers->{'content-type'} = (defined $boundary) + ? "multipart/form-data; boundary=------------------------$boundary" + : 'application/json'; + $headers->{'content-length'} = length( $content ); } From 7fdae184cee15b8cb3d6270e34b69320ff3eb309 Mon Sep 17 00:00:00 2001 From: g0t mi1k Date: Mon, 23 Jan 2023 05:55:49 +0000 Subject: [PATCH 2/2] Split edit_project -> edit_project_multipart $ perl generate.pl > ../lib/GitLab/API/v4.pm To me, this doesn't align with the function naming convention, but I couldn't see an elegant way of handling it with generate.pl --- author/generate.pl | 6 ++++++ author/sections/projects.yml | 4 ++++ lib/GitLab/API/v4.pm | 37 ++++++++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/author/generate.pl b/author/generate.pl index 76096ed..341dbb3 100755 --- a/author/generate.pl +++ b/author/generate.pl @@ -121,6 +121,12 @@ print " \$options->{$params_key} = \$params if defined \$params;\n"; } + my @code; + @code = split /\n/, $endpoint->{code} if $endpoint->{code}; + foreach my $line (@code) { + print " $line\n"; + } + print ' '; print 'return ' if $return; print "\$self->_call_rest_client( '$verb', '$path', [\@_], \$options );\n"; diff --git a/author/sections/projects.yml b/author/sections/projects.yml index 5ed0990..4e149f9 100644 --- a/author/sections/projects.yml +++ b/author/sections/projects.yml @@ -6,6 +6,10 @@ - create_project: project = POST projects? - create_project_for_user: POST projects/user/:user_id? - edit_project: PUT projects/:project_id? +- method: edit_project_multipart + spec: PUT projects/:project_id? + note: The request will have "multipart/form-data" header set for uploading files. + code: $options->{content}->{file} = $params; - fork_project: POST projects/:project_id/fork? - project_forks: forks = GET projects/:project_id/forks? - start_project: project = POST projects/:project_id/star diff --git a/lib/GitLab/API/v4.pm b/lib/GitLab/API/v4.pm index 9d960c9..28fdad9 100644 --- a/lib/GitLab/API/v4.pm +++ b/lib/GitLab/API/v4.pm @@ -7321,7 +7321,6 @@ sub create_project_for_user { $api->edit_project( $project_id, \%params, - \%params_multipart, ); Sends a C request to C. @@ -7330,21 +7329,39 @@ Sends a C request to C. sub edit_project { my $self = shift; - croak 'edit_project must be called with 1 to 3 arguments' if @_ < 1 or @_ > 3; + croak 'edit_project must be called with 1 to 2 arguments' if @_ < 1 or @_ > 2; croak 'The #1 argument ($project_id) to edit_project must be a scalar' if ref($_[0]) or (!defined $_[0]); - croak 'The #2 argument (\%params) to edit_project must be a hash ref' if defined($_[1]) and ref($_[1]) ne 'HASH'; - croak 'The last argument (\%params_multipart) to edit_project must be a hash ref' if defined($_[2]) and ref($_[2]) ne 'HASH'; - my $params_multipart = (@_ == 3) ? pop() : undef; + croak 'The last argument (\%params) to edit_project must be a hash ref' if defined($_[1]) and ref($_[1]) ne 'HASH'; my $params = (@_ == 2) ? pop() : undef; my $options = {}; $options->{decode} = 0; $options->{content} = $params if defined $params; - $self->_call_rest_client( 'PUT', 'projects/:project_id', [$_[0]], $options ); + $self->_call_rest_client( 'PUT', 'projects/:project_id', [@_], $options ); + return; +} - if (keys %$params_multipart) { - $options->{content}->{file} = $params_multipart ; - $self->_call_rest_client( 'PUT', 'projects/:project_id', [$_[0]], $options ); - } +=item edit_project_multipart + + $api->edit_project_multipart( + $project_id, + \%params, + ); + +Sends a C request to C. + +The request will have "multipart/form-data" header set for uploading files. +=cut + +sub edit_project_multipart { + my $self = shift; + croak 'edit_project_multipart must be called with 1 to 2 arguments' if @_ < 1 or @_ > 2; + croak 'The #1 argument ($project_id) to edit_project_multipart must be a scalar' if ref($_[0]) or (!defined $_[0]); + croak 'The last argument (\%params) to edit_project_multipart must be a hash ref' if defined($_[1]) and ref($_[1]) ne 'HASH'; + my $params = (@_ == 2) ? pop() : undef; + my $options = {}; + $options->{decode} = 0; + $options->{content}->{file} = $params if defined $params; + $self->_call_rest_client( 'PUT', 'projects/:project_id', [@_], $options ); return; }