Skip to content

Commit 0bdd8af

Browse files
authored
feat(HyperRequest): Allowing attaching file uploads to HyperRequests
1 parent 94009d2 commit 0bdd8af

File tree

12 files changed

+189
-32
lines changed

12 files changed

+189
-32
lines changed

.github/workflows/pr.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
runs-on: ubuntu-latest
1818
name: Tests
1919
strategy:
20-
fail-fast: true
20+
fail-fast: false
2121
matrix:
2222
cfengine: ["lucee@5", "adobe@2016", "adobe@2018"]
2323
coldbox: ["coldbox@6"]
@@ -28,16 +28,16 @@ jobs:
2828
- name: Setup Java JDK
2929
uses: actions/setup-java@v1.4.3
3030
with:
31-
java-version: 11
31+
java-version: 11
3232

3333
- name: Set Up CommandBox
3434
uses: elpete/setup-commandbox@v1.0.0
35-
35+
3636
- name: Install dependencies
3737
run: |
3838
box install
3939
box install ${{ matrix.coldbox }} --noSave
40-
40+
4141
- name: Start server
4242
run: box server start cfengine=${{ matrix.cfengine }} --noSaveSettings
4343

@@ -54,14 +54,14 @@ jobs:
5454
- name: Setup Java JDK
5555
uses: actions/setup-java@v1.4.3
5656
with:
57-
java-version: 11
57+
java-version: 11
5858

5959
- name: Set Up CommandBox
6060
uses: elpete/setup-commandbox@v1.0.0
61-
61+
6262
- name: Install CFFormat
6363
run: box install commandbox-cfformat
64-
64+
6565
- name: Run CFFormat
6666
run: box run-script format
6767

.github/workflows/prerelease.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')"
1212
runs-on: ubuntu-latest
1313
strategy:
14-
fail-fast: true
14+
fail-fast: false
1515
matrix:
1616
cfengine: ["lucee@5", "adobe@2016", "adobe@2018"]
1717
coldbox: ["coldbox@6"]
@@ -22,16 +22,16 @@ jobs:
2222
- name: Setup Java JDK
2323
uses: actions/setup-java@v1.4.3
2424
with:
25-
java-version: 11
25+
java-version: 11
2626

2727
- name: Set Up CommandBox
2828
uses: elpete/setup-commandbox@v1.0.0
29-
29+
3030
- name: Install dependencies
3131
run: |
3232
box install
3333
box install ${{ matrix.coldbox }} --noSave
34-
34+
3535
- name: Start server
3636
run: box server start cfengine=${{ matrix.cfengine }} --noSaveSettings
3737

@@ -54,17 +54,17 @@ jobs:
5454
# - name: Setup Java JDK
5555
# uses: actions/setup-java@v1.4.3
5656
# with:
57-
# java-version: 11
57+
# java-version: 11
5858

5959
# - name: Set Up CommandBox
6060
# uses: elpete/setup-commandbox@v1.0.0
61-
61+
6262
# - name: Install and Configure Semantic Release
6363
# run: |
6464
# box install commandbox-semantic-release
6565
# box config set endpoints.forgebox.APIToken=${{ secrets.FORGEBOX_TOKEN }}
6666
# box config set modules.commandbox-semantic-release.plugins='{ "VerifyConditions": "GitHubActionsConditionsVerifier@commandbox-semantic-release", "FetchLastRelease": "ForgeBoxReleaseFetcher@commandbox-semantic-release", "RetrieveCommits": "JGitCommitsRetriever@commandbox-semantic-release", "ParseCommit": "ConventionalChangelogParser@commandbox-semantic-release", "FilterCommits": "DefaultCommitFilterer@commandbox-semantic-release", "AnalyzeCommits": "DefaultCommitAnalyzer@commandbox-semantic-release", "VerifyRelease": "NullReleaseVerifier@commandbox-semantic-release", "GenerateNotes": "GitHubMarkdownNotesGenerator@commandbox-semantic-release", "UpdateChangelog": "FileAppendChangelogUpdater@commandbox-semantic-release", "CommitArtifacts": "NullArtifactsCommitter@commandbox-semantic-release", "PublishRelease": "ForgeBoxReleasePublisher@commandbox-semantic-release", "PublicizeRelease": "GitHubReleasePublicizer@commandbox-semantic-release" }'
67-
67+
6868
# - name: Run Semantic Release
6969
# env:
7070
# GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/release.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')"
1313
runs-on: ubuntu-latest
1414
strategy:
15-
fail-fast: true
15+
fail-fast: false
1616
matrix:
1717
cfengine: ["lucee@5", "adobe@2016", "adobe@2018"]
1818
coldbox: ["coldbox@6"]
@@ -23,16 +23,16 @@ jobs:
2323
- name: Setup Java JDK
2424
uses: actions/setup-java@v1.4.3
2525
with:
26-
java-version: 11
26+
java-version: 11
2727

2828
- name: Set Up CommandBox
2929
uses: elpete/setup-commandbox@v1.0.0
30-
30+
3131
- name: Install dependencies
3232
run: |
3333
box install
3434
box install ${{ matrix.coldbox }} --noSave
35-
35+
3636
- name: Start server
3737
run: box server start cfengine=${{ matrix.cfengine }} --noSaveSettings
3838

@@ -55,18 +55,18 @@ jobs:
5555
- name: Setup Java JDK
5656
uses: actions/setup-java@v1.4.3
5757
with:
58-
java-version: 11
58+
java-version: 11
5959

6060
- name: Set Up CommandBox
6161
uses: elpete/setup-commandbox@v1.0.0
62-
62+
6363
- name: Install and Configure Semantic Release
6464
run: |
6565
box install commandbox-semantic-release
6666
box config set endpoints.forgebox.APIToken=${{ secrets.FORGEBOX_TOKEN }}
6767
box config set modules.commandbox-semantic-release.targetBranch=main
6868
box config set modules.commandbox-semantic-release.plugins='{ "VerifyConditions": "GitHubActionsConditionsVerifier@commandbox-semantic-release", "FetchLastRelease": "ForgeBoxReleaseFetcher@commandbox-semantic-release", "RetrieveCommits": "JGitCommitsRetriever@commandbox-semantic-release", "ParseCommit": "ConventionalChangelogParser@commandbox-semantic-release", "FilterCommits": "DefaultCommitFilterer@commandbox-semantic-release", "AnalyzeCommits": "DefaultCommitAnalyzer@commandbox-semantic-release", "VerifyRelease": "NullReleaseVerifier@commandbox-semantic-release", "GenerateNotes": "GitHubMarkdownNotesGenerator@commandbox-semantic-release", "UpdateChangelog": "FileAppendChangelogUpdater@commandbox-semantic-release", "CommitArtifacts": "NullArtifactsCommitter@commandbox-semantic-release", "PublishRelease": "ForgeBoxReleasePublisher@commandbox-semantic-release", "PublicizeRelease": "GitHubReleasePublicizer@commandbox-semantic-release" }'
69-
69+
7070
- name: Run Semantic Release
7171
env:
7272
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,18 @@ Check if the request has a query parameter with the given name.
496496
| ---- | ------ | -------- | ------- | ----------------------------------------- |
497497
| name | string | true | | The name of the query parameter to check. |
498498

499+
##### `attach`
500+
501+
Attaches a file to the Hyper request.
502+
Also sets the Content-Type as `multipart/form-data`.
503+
Multiple files can be attached by calling `attach` multiple times before calling a send method.
504+
505+
| Name | Type | Required | Default | Description |
506+
| -------- | ------ | -------- | ------- | ------------------------------------------------- |
507+
| name | string | true | | The name of the file being uploaded. |
508+
| path | string | true | | The absolute path to the file to be uploaded. |
509+
| mimeType | string | false | | An optional mime type to associate with the file. |
510+
499511
##### `setThrowOnError`
500512

501513
Sets the throw on error property for the request. If true, error codes and status

models/CfhttpHttpClient.cfc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ component implements="HyperHttpClientInterface" {
7878
cfhttp( attributeCollection = attrCollection ) {
7979
var headers = req.getHeaders();
8080
for ( var name in headers ) {
81+
// we want to skip adding a Content-Type header when there are files
82+
// so that the CFML engines can add the correct boundary to the Content-Type
83+
if ( name == "Content-Type" && !req.getFiles().isEmpty() ) {
84+
continue;
85+
}
86+
8187
cfhttpparam(
8288
type = "header",
8389
name = name,
@@ -94,6 +100,18 @@ component implements="HyperHttpClientInterface" {
94100
);
95101
}
96102

103+
for ( var file in req.getFiles() ) {
104+
var fileAttrCollection = {
105+
type : "file",
106+
name : file.name,
107+
file : file.path
108+
};
109+
if ( file.keyExists( "mimeType" ) && !isNull( file.mimeType ) ) {
110+
fileAttrCollection[ "mimeType" ] = file.mimeType;
111+
}
112+
cfhttpparam( attributeCollection = fileAttrCollection );
113+
}
114+
97115
if ( req.hasBody() ) {
98116
if ( req.getBodyFormat() == "json" ) {
99117
cfhttpparam( type = "body", value = req.prepareBody() );

models/HyperRequest.cfc

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ component accessors="true" {
9797
*/
9898
property name="queryParams";
9999

100+
/**
101+
* An array of files to upload for the request.
102+
*/
103+
property name="files" type="array";
104+
100105
/**
101106
* Flag to throw on a cfhttp error.
102107
*/
@@ -150,6 +155,7 @@ component accessors="true" {
150155
variables.queryParams = createObject( "java", "java.util.LinkedHashMap" ).init();
151156
variables.headers = createObject( "java", "java.util.LinkedHashMap" ).init();
152157
variables.headers.put( "Content-Type", "application/json" );
158+
variables.files = [];
153159
variables.requestCallbacks = [];
154160
variables.responseCallbacks = [];
155161
// This is overwritten by the HyperBuilder if WireBox exists.
@@ -521,6 +527,35 @@ component accessors="true" {
521527
return this;
522528
}
523529

530+
/**
531+
* Attaches a file to the Hyper request.
532+
* Also sets the Content-Type as `multipart/form-data`.
533+
* Multiple files can be attached by calling `attach` multiple times before calling a send method.
534+
*
535+
* @name The name of the file being uploaded.
536+
* @path The absolute path to the file to be uploaded.
537+
* @mimeType An optional mime type to associate with the file.
538+
*
539+
* @returns The HyperRequest instance.
540+
*/
541+
function attach(
542+
required string name,
543+
required string path,
544+
string mimeType
545+
) {
546+
setBodyFormat( "formFields" );
547+
setContentType( "multipart/form-data" );
548+
var fileInfo = {
549+
name : arguments.name,
550+
path : arguments.path
551+
};
552+
if ( !isNull( arguments.mimeType ) ) {
553+
fileInfo[ "mimeType" ] = arguments.mimeType;
554+
}
555+
variables.files.append( fileInfo );
556+
return this;
557+
}
558+
524559
/**
525560
* A convenience method to set the Content-Type header.
526561
*
@@ -629,6 +664,7 @@ component accessors="true" {
629664
variables.queryParams = createObject( "java", "java.util.LinkedHashMap" ).init();
630665
variables.headers = createObject( "java", "java.util.LinkedHashMap" ).init();
631666
variables.headers.put( "Content-Type", "application/json" );
667+
variables.files = [];
632668
variables.requestCallbacks = [];
633669
variables.responseCallbacks = [];
634670
return this;
@@ -662,11 +698,11 @@ component accessors="true" {
662698
return this;
663699
}
664700

665-
/**
666-
* Clones the current request into a new HyperRequest.
667-
*
668-
* @returns A new HyperRequest instance cloned from this one.
669-
*/
701+
/**
702+
* Clones the current request into a new HyperRequest.
703+
*
704+
* @returns A new HyperRequest instance cloned from this one.
705+
*/
670706
public HyperRequest function clone() {
671707
var req = new HyperRequest();
672708
req.setInterceptorService( variables.interceptorService );
@@ -684,6 +720,7 @@ component accessors="true" {
684720
req.setReferrer( isNull( variables.referrer ) ? javacast( "null", "" ) : variables.referrer );
685721
req.setHeaders( variables.headers.clone() );
686722
req.setQueryParams( variables.queryParams.clone() );
723+
req.setFiles( duplicate( variables.files ) );
687724
req.setThrowOnError( variables.throwOnError );
688725
req.setClientCert( isNull( variables.clientCert ) ? javacast( "null", "" ) : variables.clientCert );
689726
req.setClientCertPassword(

models/HyperResponse.cfc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ component accessors="true" {
9292
function json() {
9393
if ( !isJSON( getData() ) ) {
9494
throw(
95-
type = "DeserializeJsonException",
96-
message = "The response is not json.",
97-
detail = getData()
98-
);
95+
type = "DeserializeJsonException",
96+
message = "The response is not json.",
97+
detail = getData()
98+
);
9999
}
100100
return deserializeJSON( getData() );
101101
}

tests/Application.cfc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@ component {
1515
this.mappings[ "/app" ] = testsPath & "resources/app";
1616
this.mappings[ "/coldbox" ] = testsPath & "resources/app/coldbox";
1717
this.mappings[ "/testbox" ] = rootPath & "/testbox";
18+
19+
// function onRequestStart() {
20+
// applicationStop();
21+
// abort;
22+
// }
23+
1824
}

tests/resources/app/config/Coldbox.cfc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ component {
3636
// Application Aspects
3737
handlerCaching : false,
3838
eventCaching : false,
39-
viewCaching : false
39+
viewCaching : false,
40+
customErrorTemplate : "/coldbox/system/exceptions/BugReport.cfm"
4041
};
4142

4243
// custom settings
@@ -84,7 +85,7 @@ component {
8485
* Development environment
8586
*/
8687
function development() {
87-
coldbox.customErrorTemplate = "/coldbox/system/includes/BugReport.cfm";
88+
coldbox.customErrorTemplate = "/coldbox/system/exceptions/BugReport.cfm";
8889
}
8990

9091
}

tests/resources/app/handlers/api.cfc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,47 @@ component {
3535
);
3636
}
3737

38+
function photos( event, rc, prc ) {
39+
if ( !event.valueExists( "smallPhoto" ) ) {
40+
return event.renderData(
41+
type = "text",
42+
statusCode = 422,
43+
data = "A `smallPhoto` is required."
44+
);
45+
}
46+
47+
if ( !event.valueExists( "largePhoto" ) ) {
48+
return event.renderData(
49+
type = "text",
50+
statusCode = 422,
51+
data = "A `largePhoto` is required."
52+
);
53+
}
54+
55+
var smallPhoto = fileUpload(
56+
getTempDirectory(),
57+
"smallPhoto",
58+
"*",
59+
"overwrite"
60+
);
61+
62+
var largePhoto = fileUpload(
63+
getTempDirectory(),
64+
"largePhoto",
65+
"*",
66+
"overwrite"
67+
);
68+
69+
return event.renderData(
70+
type = "json",
71+
statusCode = 201,
72+
data = {
73+
"id" : 777,
74+
"smallPhoto" : smallPhoto.serverFile,
75+
"largePhoto" : largePhoto.serverFile,
76+
"description" : rc.description
77+
}
78+
);
79+
}
80+
3881
}

0 commit comments

Comments
 (0)