Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions web/pgadmin/tools/import_export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,19 @@ def _save_import_export_settings(settings):
_VALID_LOG_VERBOSITY = frozenset({'default', 'verbose'})


def _copy_filename_literal(filename):
"""The file name as psql's \\copy expects it: wrapped in single quotes
with only an embedded single quote doubled.

psql parses the file name of \\copy itself, and that token has no
backslash escapes and no E'...' form. qtLiteral() must not be used for
it: for a value containing a backslash it returns an escape-string
literal, which psql reads as a file named "E" followed by text it
passes on to the server.
"""
return "'" + filename.replace("'", "''") + "'"


def _identifier_strings(value):
"""The identifier strings columns_to_string() would feed to qtIdent()
for value, whatever JSON type it arrives as. A list yields its string
Expand Down Expand Up @@ -593,6 +606,8 @@ def create_import_export_job(sid):
# Create the COPY FROM/TO from template
temp_path = 'import_export/sql/#{0}#/cmd.sql'.format(manager.version)
query = render_template(temp_path, conn=conn, data=data, columns=cols,
filename=_copy_filename_literal(
data['filename']),
not_null_columns=not_null_cols,
null_columns=null_cols,
force_quote_columns=quote_cols)
Expand All @@ -618,6 +633,7 @@ def create_import_export_job(sid):
try:
launch_query = render_template(
temp_path, conn=conn, data=dict(data, filename=utility_file),
filename=_copy_filename_literal(utility_file),
columns=cols, not_null_columns=not_null_cols,
null_columns=null_cols, force_quote_columns=quote_cols)
except Exception:
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
\copy {% if data.query %}({{ data.query }}) {% else %}{{ conn|qtIdent(data.schema, data.table) }}{% if columns %}{{ columns }} {% endif %}{% endif %} {% if data.is_import %}FROM{% else %}TO{% endif %} {{ data.filename|qtLiteral(conn) }} WITH(FORMAT {{data.format}}{% if data.delimiter and data.format != 'binary' and data.delimiter == '[tab]' %}, DELIMITER E'\t'{% elif data.format != 'binary' and data.delimiter %}, DELIMITER {{ data.delimiter|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.header %}, HEADER{% endif %}{% if data.freeze %}, FREEZE{% endif %}{% if data.encoding %}, ENCODING {{ data.encoding|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.quote %}, QUOTE {{ data.quote|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.null_string %}, NULL {{ data.null_string|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.is_import and data.default_string %}, DEFAULT {{ data.default_string|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.escape %}, ESCAPE {{ data.escape|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.is_import and not_null_columns %}, FORCE_NOT_NULL {{ not_null_columns }}{% endif %}{% if data.format == 'csv' and data.is_import and null_columns %}, FORCE_NULL {{ null_columns }}{% endif %}{% if data.format == 'csv' and not data.is_import and force_quote_columns %}, FORCE_QUOTE {{ force_quote_columns }}{% endif %});
\copy {% if data.query %}({{ data.query }}) {% else %}{{ conn|qtIdent(data.schema, data.table) }}{% if columns %}{{ columns }} {% endif %}{% endif %} {% if data.is_import %}FROM{% else %}TO{% endif %} {{ filename }} WITH(FORMAT {{data.format}}{% if data.delimiter and data.format != 'binary' and data.delimiter == '[tab]' %}, DELIMITER E'\t'{% elif data.format != 'binary' and data.delimiter %}, DELIMITER {{ data.delimiter|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.header %}, HEADER{% endif %}{% if data.freeze %}, FREEZE{% endif %}{% if data.encoding %}, ENCODING {{ data.encoding|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.quote %}, QUOTE {{ data.quote|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.null_string %}, NULL {{ data.null_string|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.is_import and data.default_string %}, DEFAULT {{ data.default_string|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.escape %}, ESCAPE {{ data.escape|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.is_import and not_null_columns %}, FORCE_NOT_NULL {{ not_null_columns }}{% endif %}{% if data.format == 'csv' and data.is_import and null_columns %}, FORCE_NULL {{ null_columns }}{% endif %}{% if data.format == 'csv' and not data.is_import and force_quote_columns %}, FORCE_QUOTE {{ force_quote_columns }}{% endif %});
Original file line number Diff line number Diff line change
@@ -1 +1 @@
\copy {% if data.query %}({{ data.query }}) {% else %}{{ conn|qtIdent(data.schema, data.table) }}{% if columns %}{{ columns }} {% endif %}{% endif %} {% if data.is_import %}FROM{% else %}TO{% endif %} {{ data.filename|qtLiteral(conn) }} WITH(FORMAT {{data.format}}{% if data.delimiter and data.format != 'binary' and data.delimiter == '[tab]' %}, DELIMITER E'\t'{% elif data.format != 'binary' and data.delimiter %}, DELIMITER {{ data.delimiter|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.header %}, HEADER{% endif %}{% if data.freeze %}, FREEZE{% endif %}{% if data.encoding %}, ENCODING {{ data.encoding|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.quote %}, QUOTE {{ data.quote|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.null_string %}, NULL {{ data.null_string|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.is_import and data.default_string %}, DEFAULT {{ data.default_string|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.escape %}, ESCAPE {{ data.escape|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.is_import and not_null_columns %}, FORCE_NOT_NULL {{ not_null_columns }}{% endif %}{% if data.format == 'csv' and data.is_import and null_columns %}, FORCE_NULL {{ null_columns }}{% endif %}{% if data.format == 'csv' and not data.is_import and force_quote_columns %}, FORCE_QUOTE {{ force_quote_columns }}{% endif %}{% if data.format != 'binary' and data.is_import and data.on_error == 'ignore' %}, ON_ERROR {{ data.on_error }}{% endif %}{% if data.format != 'binary' and data.is_import and data.log_verbosity and data.on_error == 'ignore' %}, LOG_VERBOSITY {{ data.log_verbosity }}{% endif %});
\copy {% if data.query %}({{ data.query }}) {% else %}{{ conn|qtIdent(data.schema, data.table) }}{% if columns %}{{ columns }} {% endif %}{% endif %} {% if data.is_import %}FROM{% else %}TO{% endif %} {{ filename }} WITH(FORMAT {{data.format}}{% if data.delimiter and data.format != 'binary' and data.delimiter == '[tab]' %}, DELIMITER E'\t'{% elif data.format != 'binary' and data.delimiter %}, DELIMITER {{ data.delimiter|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.header %}, HEADER{% endif %}{% if data.freeze %}, FREEZE{% endif %}{% if data.encoding %}, ENCODING {{ data.encoding|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.quote %}, QUOTE {{ data.quote|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.null_string %}, NULL {{ data.null_string|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.is_import and data.default_string %}, DEFAULT {{ data.default_string|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.escape %}, ESCAPE {{ data.escape|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.is_import and not_null_columns %}, FORCE_NOT_NULL {{ not_null_columns }}{% endif %}{% if data.format == 'csv' and data.is_import and null_columns %}, FORCE_NULL {{ null_columns }}{% endif %}{% if data.format == 'csv' and not data.is_import and force_quote_columns %}, FORCE_QUOTE {{ force_quote_columns }}{% endif %}{% if data.format != 'binary' and data.is_import and data.on_error == 'ignore' %}, ON_ERROR {{ data.on_error }}{% endif %}{% if data.format != 'binary' and data.is_import and data.log_verbosity and data.on_error == 'ignore' %}, LOG_VERBOSITY {{ data.log_verbosity }}{% endif %});
Original file line number Diff line number Diff line change
@@ -1 +1 @@
\copy {% if data.query %}({{ data.query }}) {% else %}{{ conn|qtIdent(data.schema, data.table) }}{% if columns %}{{ columns }} {% endif %}{% endif %} {% if data.is_import %}FROM{% else %}TO{% endif %} {{ data.filename|qtLiteral(conn) }} WITH(FORMAT {{data.format}}{% if data.delimiter and data.format != 'binary' and data.delimiter == '[tab]' %}, DELIMITER E'\t'{% elif data.format != 'binary' and data.delimiter %}, DELIMITER {{ data.delimiter|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.header %}, HEADER{% endif %}{% if data.freeze %}, FREEZE{% endif %}{% if data.encoding %}, ENCODING {{ data.encoding|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.quote %}, QUOTE {{ data.quote|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.null_string %}, NULL {{ data.null_string|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.escape %}, ESCAPE {{ data.escape|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.is_import and not_null_columns %}, FORCE_NOT_NULL {{ not_null_columns }}{% endif %}{% if data.format == 'csv' and data.is_import and null_columns %}, FORCE_NULL {{ null_columns }}{% endif %}{% if data.format == 'csv' and not data.is_import and force_quote_columns %}, FORCE_QUOTE {{ force_quote_columns }}{% endif %});
\copy {% if data.query %}({{ data.query }}) {% else %}{{ conn|qtIdent(data.schema, data.table) }}{% if columns %}{{ columns }} {% endif %}{% endif %} {% if data.is_import %}FROM{% else %}TO{% endif %} {{ filename }} WITH(FORMAT {{data.format}}{% if data.delimiter and data.format != 'binary' and data.delimiter == '[tab]' %}, DELIMITER E'\t'{% elif data.format != 'binary' and data.delimiter %}, DELIMITER {{ data.delimiter|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.header %}, HEADER{% endif %}{% if data.freeze %}, FREEZE{% endif %}{% if data.encoding %}, ENCODING {{ data.encoding|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.quote %}, QUOTE {{ data.quote|qtLiteral(conn) }}{% endif %}{% if data.format != 'binary' and data.null_string %}, NULL {{ data.null_string|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.escape %}, ESCAPE {{ data.escape|qtLiteral(conn) }}{% endif %}{% if data.format == 'csv' and data.is_import and not_null_columns %}, FORCE_NOT_NULL {{ not_null_columns }}{% endif %}{% if data.format == 'csv' and data.is_import and null_columns %}, FORCE_NULL {{ null_columns }}{% endif %}{% if data.format == 'csv' and not data.is_import and force_quote_columns %}, FORCE_QUOTE {{ force_quote_columns }}{% endif %});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

import os

from jinja2 import Environment, FileSystemLoader

from pgadmin.utils.route import BaseTestGenerator

TEMPLATES_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
'templates', 'import_export', 'sql')


class IECopyFilenameLiteralTest(BaseTestGenerator):
"""Test the _copy_filename_literal function.

psql parses the file name of \\copy itself: the token is optionally
wrapped in single quotes, an embedded single quote is doubled, and
there are no backslash escapes and no E'...' form. A backslash must
therefore reach psql unchanged, in a plain '...' literal.
"""

scenarios = [
('Plain file name', dict(
filename='/tmp/a.csv', expected="'/tmp/a.csv'")),
('File name with a space', dict(
filename='/tmp/a b.csv', expected="'/tmp/a b.csv'")),
('Backslash is left as it is', dict(
filename='/tmp/a\\b.csv', expected="'/tmp/a\\b.csv'")),
('Trailing backslash does not escape the closing quote', dict(
filename='/tmp/a\\', expected="'/tmp/a\\'")),
('Single quote is doubled', dict(
filename="/tmp/a'b.csv", expected="'/tmp/a''b.csv'")),
('Backslash before a single quote', dict(
filename="/tmp/a\\'b.csv", expected="'/tmp/a\\''b.csv'")),
('File descriptor path', dict(
filename='/dev/fd/7', expected="'/dev/fd/7'")),
]

def runTest(self):
from pgadmin.tools.import_export import _copy_filename_literal
self.assertEqual(
_copy_filename_literal(self.filename), self.expected)


class IECopyFilenameTemplateTest(BaseTestGenerator):
"""The \\copy templates must write the file name they are given as it
is, and must not pass it through qtLiteral(), which turns a value
containing a backslash into an E'...' literal that psql reads as a
file named "E".
"""

scenarios = [
('Export, {0} template'.format(version), dict(
version=version, is_import=False))
for version in ('default', '16_plus', '17_plus')
] + [
('Import, {0} template'.format(version), dict(
version=version, is_import=True))
for version in ('default', '16_plus', '17_plus')
]

def runTest(self):
from pgadmin.tools.import_export import _copy_filename_literal

env = Environment(loader=FileSystemLoader(TEMPLATES_DIR))
env.filters['qtIdent'] = lambda conn, *names: '.'.join(names)
env.filters['qtLiteral'] = \
lambda value, conn: 'qtLiteral({0})'.format(value)

filename = '/tmp/a\\b.csv'
data = dict(
schema='public', table='t', is_import=self.is_import,
filename=filename, format='csv', delimiter=',',
encoding='UTF8')
cmd = env.get_template(self.version + '/cmd.sql').render(
conn=None, data=data, columns=None,
filename=_copy_filename_literal(filename),
not_null_columns=None, null_columns=None,
force_quote_columns=None)

self.assertIn(
"{0} '/tmp/a\\b.csv' WITH(FORMAT csv".format(
'FROM' if self.is_import else 'TO'), cmd)
self.assertNotIn("E'/tmp", cmd)
self.assertNotIn('qtLiteral(/tmp', cmd)
# The option values are parsed by the server and keep qtLiteral().
self.assertIn('DELIMITER qtLiteral(,)', cmd)