Skip to content

Commit

Permalink
Merge pull request #4069 from FlowFuse/3972-multi-line-env-vars
Browse files Browse the repository at this point in the history
Support multiline env vars
  • Loading branch information
joepavitt committed Jun 26, 2024
2 parents a0ea7f0 + f8ebcb3 commit 9454799
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 21 deletions.
59 changes: 48 additions & 11 deletions frontend/src/pages/admin/Template/sections/Environment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class="w-full max-w-5xl text-sm"
:show-search="true"
search-placeholder="Search environment variables..."
:columns="[,,,]"
:columns="editTemplate ? [,,,,] : [,,,]"
:noDataMessage="noDataMessage"
>
<template #actions>
Expand Down Expand Up @@ -43,24 +43,30 @@
<FormRow
v-model="item.name"
class="font-mono"
containerClass="w-full"
:containerClass="'w-full' + (!readOnly && (editTemplate || item.policy === undefined)) ? ' env-cell-uneditable':''"
:inputClass="item.deprecated ? 'w-full text-yellow-700 italic' : 'w-full'"
:error="item.error"
:disabled="item.encrypted"
value-empty-text=""
:type="(!readOnly && (editTemplate || item.policy === undefined))?'text':'uneditable'"
/>
</td>
<td class="ff-data-table--cell !p-1 border w-3/5 align-top" :class="{'align-middle':item.encrypted}">
<td class="ff-data-table--cell !p-1 border w-3/5 align-top max-w-xl" :class="{'align-middle':item.encrypted}">
<div v-if="!item.encrypted" class="w-full">
<FormRow
v-model="item.value"
class="font-mono"
containerClass="w-full"
:inputClass="item.deprecated ? 'text-yellow-700 italic' : ''"
value-empty-text=""
:type="(!readOnly && (editTemplate || item.policy === undefined || item.policy))?'text':'uneditable'"
/>
<template v-if="(!readOnly && (editTemplate || item.policy === undefined || item.policy))">
<!-- editable -->
<textarea v-model="item.value" :class="'w-full font-mono max-h-40' + ((item.value && item.value.split('\n').length > 1) ? ' h-20' : ' h-8') + (item.deprecated ? ' text-yellow-700 italic' : '')" />
</template>
<template v-else>
<FormRow
v-model="item.value"
class="font-mono"
containerClass="w-full env-cell-uneditable"
:inputClass="item.deprecated ? 'text-yellow-700 italic' : ''"
value-empty-text=""
:type="'uneditable'"
/>
</template>
</div>
<div v-else class="pt-1 text-gray-400"><LockClosedIcon class="inline w-4" /> encrypted</div>
</td>
Expand Down Expand Up @@ -312,3 +318,34 @@ export default {
}
}
</script>
<style scoped>
.ff-data-table--cell textarea {
resize: vertical;
max-height: 10rem; /* 160px approx ~8 lines, after which user will need to scroll */
/* Below styles emulate the text control in a form row */
border: 1px solid #D1D5DB;
border-radius: 6px;
/* height: 32px; */
padding: 6px;
min-height: 32px; /* align with item in cell-1*/
width: 100%;
display: flex;
gap: 0px;
align-items: center;
background-color: white;
border-color: #D1D5DB;
}
.ff-data-table--cell .env-cell-uneditable {
max-height: 10rem; /* 160px approx ~8 lines, after which user will need to scroll */
overflow: auto;
white-space: pre;
cursor: default;
}
.ff-data-table--cell .env-cell-uneditable input {
cursor: default;
}
.ff-data-table--cell div.uneditable {
cursor: default;
}
</style>
84 changes: 74 additions & 10 deletions frontend/src/pages/admin/Template/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,30 +299,94 @@ function prepareTemplateForEdit (template) {
/**
* Parse .env file data into an object
* NOTES:
* * Variable expansion is not supported
* * Comments are not supported
* * Only simple key/value pairs are supported
* * Only single-line values are supported
* * SPECIFICATION:
* * Simple key/value pairs are supported
* * Multiline values are supported
* * Single and double quoted values are supported
* * Single line comments are skipped
* * Leading and trailing whitespace is trimmed if unquoted
* * Leading and trailing whitespace is preserved if quoted
* * Blank lines are skipped
* * Windows and Mac newlines are converted to Unix newlines (opinionated)
* * UNSUPPORTED: The following features are not supported:
* * Variable expansion is not supported
* * Escaped quotes are not supported
* * Multiline keys are not supported
* * Multiline comments are not supported
* @param {String} data The env file data to parse
* @returns key/value pairs
*/
function parseDotEnv (data) {
const result = {}
const lines = data.split('\n')

// For convenience and simplicity (MVP), force all newlines to be \n
const newline = '\n'
data = data.replace(/\r\n/g, newline).replace(/\r/g, newline)

const lines = data.split(newline)
let currentKey = null
let currentValue = ''
let isMultiline = false
let quoteType = null

for (const line of lines) {
// Skip empty lines and comments that are not part of a multiline value
if (!isMultiline && (!line.trim() || line.trim().startsWith('#'))) {
continue
}

// Handle continuation of multiline value
if (isMultiline) {
currentValue += newline + line
if (line.trim().endsWith(quoteType)) {
result[currentKey] = currentValue.slice(0, -1)
currentKey = null
currentValue = ''
isMultiline = false
quoteType = null
}
continue
}

// Match key/value pair
const match = line.match(/^([^=:#]+?)[=:](.*)/)
if (match) {
const key = match[1].trim()
let value // get value from match[2], trimming any surrounding quotes
const valueMatch = match[2].trim().match(/^(['"]?)(.+)\1$/)
const value = match[2].trim()

// Detect multiline value start
const valueMatch = value.match(/^(['"])([\s\S]*)\1$/)
if (valueMatch) {
value = valueMatch[2]
result[key] = valueMatch[2]
} else {
value = match[2].trim()
if (value.startsWith('"') || value.startsWith("'")) {
isMultiline = true
quoteType = value[0]
currentKey = key
currentValue = value.slice(1) // Remove starting quote
if (currentValue.endsWith(quoteType)) {
result[key] = currentValue.slice(0, -1) // Remove ending quote if it's a single-line quoted value
isMultiline = false
currentKey = null
currentValue = ''
quoteType = null
}
} else if (value.endsWith('\\')) {
isMultiline = true
currentKey = key
currentValue = value.slice(0, -1) // Remove trailing backslash
} else {
result[key] = value
}
}
result[key] = value
}
}

// Handle case where file ends during a multiline value
if (isMultiline && currentKey) {
result[currentKey] = currentValue
}

return result
}

Expand Down
67 changes: 67 additions & 0 deletions test/unit/frontend/utils/admin/Template/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,71 @@ describe('dotenv', () => {
const env = parseDotEnv('a="b"\nb={"c":1,"d":"e"}\nf=don\'t drop the "quote"s')
expect(env).toEqual({ a: 'b', b: '{"c":1,"d":"e"}', f: 'don\'t drop the "quote"s' })
})
test('parses multiline values', () => {
const envData = `BASIC=basic
# prev line deliberately blank
AFTER_BLANK=after_blank
EMPTY=
SINGLE_QUOTES='single_line_single_line'
SINGLE_QUOTES_SPACED=' single quotes single line untrimmed '
DOUBLE_QUOTES="double_quotes_single_line"
DOUBLE_QUOTES_SPACED=" double quotes single line untrimmed "
NEWLINES_QUOTED="expand\nnew\nlines"
NEWLINES_SINGLED_QUOTED='expand\nnew\nlines'
DONT_EXPAND=dont\\nexpand\\nescpated\\nnewlines
# COMMENTS=excludes comment that looks like a key-value pair
EQUAL_SIGNS=handles_equals=sign
JSON_UNQUOTED={"foo": "bar"}
JSON_QUOTED='{"foo": "bar"}'
JSON_MULTILINE='{
"foo": "bar"
}'
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
USERNAME=therealnerdybeast@example.tld
SPACED_KEY = should be trimmed
MULTI_DOUBLE_QUOTED="THIS IS A
MULTILINE
STRING"
MULTI_SINGLE_QUOTED='THIS IS A
MULTILINE
STRING'
MULTI_WITH_BLANK_AND_COMMENT="SHOULD
KEEP EMPTY
AND KEEP LINE STARTING WITH #
# a line starting with a hash
WHEN IT IS QUOTED"`
const env = parseDotEnv(envData)
expect(env).toEqual({
BASIC: 'basic',
AFTER_BLANK: 'after_blank',
EMPTY: '',
SINGLE_QUOTES: 'single_line_single_line',
SINGLE_QUOTES_SPACED: ' single quotes single line untrimmed ',
DOUBLE_QUOTES: 'double_quotes_single_line',
DOUBLE_QUOTES_SPACED: ' double quotes single line untrimmed ',
NEWLINES_QUOTED: 'expand\nnew\nlines',
NEWLINES_SINGLED_QUOTED: 'expand\nnew\nlines',
DONT_EXPAND: 'dont\\nexpand\\nescpated\\nnewlines',
EQUAL_SIGNS: 'handles_equals=sign',
JSON_UNQUOTED: '{"foo": "bar"}',
JSON_QUOTED: '{"foo": "bar"}',
JSON_MULTILINE: '{\n "foo": "bar"\n}',
TRIM_SPACE_FROM_UNQUOTED: 'some spaced out string',
USERNAME: 'therealnerdybeast@example.tld',
SPACED_KEY: 'should be trimmed',
MULTI_DOUBLE_QUOTED: 'THIS IS A\nMULTILINE\nSTRING',
MULTI_SINGLE_QUOTED: 'THIS IS A\nMULTILINE\nSTRING',
MULTI_WITH_BLANK_AND_COMMENT: 'SHOULD\n' +
'KEEP EMPTY\n' +
'\n' +
'AND KEEP LINE STARTING WITH #\n' +
'# a line starting with a hash\n' +
'WHEN IT IS QUOTED'
})
})
})

0 comments on commit 9454799

Please sign in to comment.