mirror of
https://github.com/lyx0/yaf.git
synced 2024-11-13 19:49:53 +01:00
Merge pull request #7 from leon-richardt/feat/scrub-exif
feat: add option to scrub EXIF tags on image files
This commit is contained in:
commit
b3f6b5a590
12 changed files with 662 additions and 46 deletions
50
README.md
50
README.md
|
@ -1,5 +1,5 @@
|
||||||
# jaf - Just Another Fileshare
|
# jaf - Just Another Fileshare
|
||||||
jaf is a simple, zero-dependency Go program to handle file uploads.
|
jaf is a simple Go program to handle file uploads.
|
||||||
If you also want to serve the uploaded files, consider a web server like [nginx](https://nginx.org/en/).
|
If you also want to serve the uploaded files, consider a web server like [nginx](https://nginx.org/en/).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
@ -28,19 +28,51 @@ Port: 4711
|
||||||
LinkPrefix: https://jaf.example.com/
|
LinkPrefix: https://jaf.example.com/
|
||||||
FileDir: /var/www/jaf/
|
FileDir: /var/www/jaf/
|
||||||
LinkLength: 5
|
LinkLength: 5
|
||||||
|
ScrubExif: true
|
||||||
|
# Both IDs also refer to the "Orientation" tag, included for illustrative purposes only
|
||||||
|
ExifAllowedIds: 0x0112 274
|
||||||
|
ExifAllowedPaths: IFD/Orientation
|
||||||
|
ExifAbortOnError: true
|
||||||
```
|
```
|
||||||
|
|
||||||
Option | Use
|
Option | Use
|
||||||
------------ | -------------------------------------------------------------------
|
------------------ | -------------------------------------------------------------------
|
||||||
`Port` | the port number jaf will listen on
|
`Port` | the port number jaf will listen on
|
||||||
`LinkPrefix` | a string that will be prepended to the file name generated by jaf
|
`LinkPrefix` | a string that will be prepended to the file name generated by jaf
|
||||||
`FileDir` | path to the directory jaf will save uploaded files in
|
`FileDir` | path to the directory jaf will save uploaded files in
|
||||||
`LinkLength` | the number of characters the generated file name is allowed to have
|
`LinkLength` | the number of characters the generated file name is allowed to have
|
||||||
|
`ScrubExif` | whether to remove EXIF tags from uploaded JPEG and PNG images (`true` or `false`)
|
||||||
|
`ExifAllowedIds` | a space-separated list of EXIF tag IDs that should be preserved through EXIF scrubbing (only relevant if `ScrubExif` is `true`)
|
||||||
|
`ExifAllowedPaths` | a space-separated list of EXIF tag paths that should be preserved through EXIF scrubbing (only relevant if `ScrubExif` is `true`)
|
||||||
|
`ExifAbortOnError` | whether to abort JPEG and PNG uploads if an error occurs during EXIF scrubbing (only relevant if `ScrubExif` is `true`)
|
||||||
|
|
||||||
|
|
||||||
Make sure the user running jaf has suitable permissions to read, and write to, `FileDir`.
|
Make sure the user running jaf has suitable permissions to read, and write to, `FileDir`.
|
||||||
Also note that `LinkLength` directly relates to the number of files that can be saved.
|
Also note that `LinkLength` directly relates to the number of files that can be saved.
|
||||||
Since jaf only uses alphanumeric characters for file name generation, a maximum of `(26 + 26 + 10)^LinkLength` names can be generated.
|
Since jaf only uses alphanumeric characters for file name generation, a maximum of `(26 + 26 + 10)^LinkLength` names can be generated.
|
||||||
|
|
||||||
|
#### A Note on EXIF Scrubbing
|
||||||
|
EXIF scrubbing can be enabled via the `ScrubExif` config key.
|
||||||
|
When enabled, all standard EXIF tags are removed on uploaded JPEG and PNG images per default.
|
||||||
|
It is meant as a last-line "defense mechanism" against leaking PII, such as GPS information on pictures.
|
||||||
|
**If possible, you should always prefer disabling capturing potentially sensitive EXIF tags when creating the images!**
|
||||||
|
|
||||||
|
Obviously, EXIF tags serve a purpose and you may want to keep _some_ of the information, e.g., image orientation.
|
||||||
|
The `ExifAllowedIds` and `ExifAllowedPaths` config keys can be used to selectively allow specific tags to survive the scrubbing.
|
||||||
|
The IDs for standard tags can be found in [1].
|
||||||
|
You may specify tag IDs in decimal and hexadecimal notation.
|
||||||
|
(In the latter case, the ID _must_ start with `0x`.)
|
||||||
|
|
||||||
|
The path specification for `ExifAllowedPaths` relies on the format implemented in [`go-exif`](https://github.com/dsoprea/go-exif) which is "documented" in machine-readable format in [2].
|
||||||
|
Multiple paths can be specified, separated by a space.
|
||||||
|
The path format is as follows:
|
||||||
|
|
||||||
|
1. For tags in the main section: `IFD/<GroupName>/<FieldName>`.
|
||||||
|
Examples: `IFD/Orientation`, `IFD/Exif/Flash`, `IFD/GPSInfo/GPSTimeStamp`.
|
||||||
|
You will probably want to use both [1] and [2] in combination if you plan to specify allowed tags by path.
|
||||||
|
|
||||||
|
2. Tags in the thumbnail section follow the same format but paths start with `IFD1/` instead of `IFD`.
|
||||||
|
|
||||||
### nginx
|
### nginx
|
||||||
If you use a reverse-proxy to forward requests to jaf, make sure to correctly forward the original request headers.
|
If you use a reverse-proxy to forward requests to jaf, make sure to correctly forward the original request headers.
|
||||||
For nginx, this is achieved via the `proxy_pass_request_headers on;` option.
|
For nginx, this is achieved via the `proxy_pass_request_headers on;` option.
|
||||||
|
@ -90,3 +122,7 @@ Note that you may have to add additional header fields to the request, e.g. if y
|
||||||
## Inspiration
|
## Inspiration
|
||||||
- [i](https://github.com/fourtf/i) by [fourtf](https://github.com/fourtf) – a project very similar in scope and size
|
- [i](https://github.com/fourtf/i) by [fourtf](https://github.com/fourtf) – a project very similar in scope and size
|
||||||
- [filehost](https://github.com/nuuls/filehost) by [nuuls](https://github.com/nuuls) – a more integrated, fully-fledged solution that offers a web interface and also serves the files
|
- [filehost](https://github.com/nuuls/filehost) by [nuuls](https://github.com/nuuls) – a more integrated, fully-fledged solution that offers a web interface and also serves the files
|
||||||
|
|
||||||
|
|
||||||
|
[1]: https://exiv2.org/tags.html
|
||||||
|
[2]: https://github.com/dsoprea/go-exif/blob/a6301f85c82b0de82ceb8501f3c4a73ea7df01c2/assets/tags.yaml
|
||||||
|
|
89
config.go
89
config.go
|
@ -13,10 +13,14 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int
|
Port int
|
||||||
LinkPrefix string
|
LinkPrefix string
|
||||||
FileDir string
|
FileDir string
|
||||||
LinkLength int
|
LinkLength int
|
||||||
|
ScrubExif bool
|
||||||
|
ExifAllowedIds []uint16
|
||||||
|
ExifAllowedPaths []string
|
||||||
|
ExifAbortOnError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigFromFile(filePath string) (*Config, error) {
|
func ConfigFromFile(filePath string) (*Config, error) {
|
||||||
|
@ -32,10 +36,14 @@ func ConfigFromFile(filePath string) (*Config, error) {
|
||||||
log.SetPrefix("config.FromFile > ")
|
log.SetPrefix("config.FromFile > ")
|
||||||
|
|
||||||
retval := &Config{
|
retval := &Config{
|
||||||
Port: 4711,
|
Port: 4711,
|
||||||
LinkPrefix: "https://jaf.example.com/",
|
LinkPrefix: "https://jaf.example.com/",
|
||||||
FileDir: "/var/www/jaf/",
|
FileDir: "/var/www/jaf/",
|
||||||
LinkLength: 5,
|
LinkLength: 5,
|
||||||
|
ScrubExif: true,
|
||||||
|
ExifAllowedIds: []uint16{},
|
||||||
|
ExifAllowedPaths: []string{},
|
||||||
|
ExifAbortOnError: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
|
@ -47,13 +55,15 @@ func ConfigFromFile(filePath string) (*Config, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := strings.Split(line, ": ")
|
key, val, found := strings.Cut(line, ":")
|
||||||
if len(tokens) != 2 {
|
|
||||||
|
if !found {
|
||||||
log.Printf("unexpected line: \"%s\", ignoring\n", line)
|
log.Printf("unexpected line: \"%s\", ignoring\n", line)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key, val := strings.TrimSpace(tokens[0]), strings.TrimSpace(tokens[1])
|
key = strings.TrimSpace(key)
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
case "Port":
|
case "Port":
|
||||||
|
@ -74,6 +84,63 @@ func ConfigFromFile(filePath string) (*Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
retval.LinkLength = parsed
|
retval.LinkLength = parsed
|
||||||
|
case "ScrubExif":
|
||||||
|
parsed, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
retval.ScrubExif = parsed
|
||||||
|
case "ExifAllowedIds":
|
||||||
|
if val == "" {
|
||||||
|
// No IDs specified at all
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
stringIds := strings.Split(val, " ")
|
||||||
|
|
||||||
|
parsedIds := make([]uint16, 0, len(stringIds))
|
||||||
|
for _, stringId := range stringIds {
|
||||||
|
var parsed uint64
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if strings.HasPrefix(stringId, "0x") {
|
||||||
|
// Parse as a hexadecimal number
|
||||||
|
hexStringId := strings.Replace(stringId, "0x", "", 1)
|
||||||
|
parsed, err = strconv.ParseUint(hexStringId, 16, 16)
|
||||||
|
} else {
|
||||||
|
// Parse as a decimal number
|
||||||
|
parsed, err = strconv.ParseUint(stringId, 10, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Could not parse ID from: \"%s\", ignoring. Error: %s\n",
|
||||||
|
stringId,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIds = append(parsedIds, uint16(parsed))
|
||||||
|
}
|
||||||
|
|
||||||
|
retval.ExifAllowedIds = parsedIds
|
||||||
|
case "ExifAllowedPaths":
|
||||||
|
if val == "" {
|
||||||
|
// No paths specified at all
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := strings.Split(val, " ")
|
||||||
|
retval.ExifAllowedPaths = paths
|
||||||
|
case "ExifAbortOnError":
|
||||||
|
parsed, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
retval.ExifAbortOnError = parsed
|
||||||
default:
|
default:
|
||||||
log.Printf("unexpected key: \"%s\", ignoring\n", key)
|
log.Printf("unexpected key: \"%s\", ignoring\n", key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,21 @@ import (
|
||||||
|
|
||||||
func assertEqual[S comparable](have S, want S, t *testing.T) {
|
func assertEqual[S comparable](have S, want S, t *testing.T) {
|
||||||
if have != want {
|
if have != want {
|
||||||
t.Error("have:", have, ", want:", want, "\n")
|
t.Error("have:", have, ", want:", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqualSlice[S comparable](have []S, want []S, t *testing.T) {
|
||||||
|
if len(have) != len(want) {
|
||||||
|
t.Error("lengths differ! have:", len(have), ", want:", len(want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range want {
|
||||||
|
if have[i] != want[i] {
|
||||||
|
t.Error("slices differ at position", i, ":", have[i], "!=", want[i])
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,4 +34,8 @@ func TestConfigFromFile(t *testing.T) {
|
||||||
assertEqual(config.LinkPrefix, "https://jaf.example.com/", t)
|
assertEqual(config.LinkPrefix, "https://jaf.example.com/", t)
|
||||||
assertEqual(config.FileDir, "/var/www/jaf/", t)
|
assertEqual(config.FileDir, "/var/www/jaf/", t)
|
||||||
assertEqual(config.LinkLength, 5, t)
|
assertEqual(config.LinkLength, 5, t)
|
||||||
|
assertEqual(config.ScrubExif, true, t)
|
||||||
|
assertEqualSlice(config.ExifAllowedIds, []uint16{0x0112, 274}, t)
|
||||||
|
assertEqualSlice(config.ExifAllowedPaths, []string{"IFD/Orientation"}, t)
|
||||||
|
assertEqual(config.ExifAbortOnError, true, t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,3 +3,8 @@ Port: 4711
|
||||||
LinkPrefix: https://jaf.example.com/
|
LinkPrefix: https://jaf.example.com/
|
||||||
FileDir: /var/www/jaf/
|
FileDir: /var/www/jaf/
|
||||||
LinkLength: 5
|
LinkLength: 5
|
||||||
|
ScrubExif: true
|
||||||
|
# Both IDs also refer to the "Orientation" tag, included for illustrative purposes only
|
||||||
|
ExifAllowedIds: 0x0112 274
|
||||||
|
ExifAllowedPaths: IFD/Orientation
|
||||||
|
ExifAbortOnError: true
|
||||||
|
|
267
exifscrubber/exifscrubber.go
Normal file
267
exifscrubber/exifscrubber.go
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
package exifscrubber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
exif "github.com/dsoprea/go-exif/v3"
|
||||||
|
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||||
|
jis "github.com/dsoprea/go-jpeg-image-structure/v2"
|
||||||
|
pis "github.com/dsoprea/go-png-image-structure/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrUnknownFileType = errors.New("can't scrub EXIF for this file type")
|
||||||
|
|
||||||
|
type ExifScrubber struct {
|
||||||
|
includedTagIds []uint16
|
||||||
|
includedTagPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExifScrubber(includedTagIds []uint16, includedTagPaths []string) ExifScrubber {
|
||||||
|
return ExifScrubber{
|
||||||
|
includedTagIds: includedTagIds,
|
||||||
|
includedTagPaths: includedTagPaths,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scrubber *ExifScrubber) ScrubExif(fileData []byte) ([]byte, error) {
|
||||||
|
// Try scrubbing using JPEG package
|
||||||
|
jpegParser := jis.NewJpegMediaParser()
|
||||||
|
if jpegParser.LooksLikeFormat(fileData) {
|
||||||
|
intfc, err := jpegParser.ParseBytes(fileData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentList := intfc.(*jis.SegmentList)
|
||||||
|
rootIfd, _, err := segmentList.Exif()
|
||||||
|
if err != nil {
|
||||||
|
if err == exif.ErrNoExif {
|
||||||
|
// Incoming data contained no EXIF in the first place so we can return the original
|
||||||
|
return fileData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredIb, err := scrubber.filteringIfdBuilder(rootIfd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
segmentList.SetExif(filteredIb)
|
||||||
|
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
err = segmentList.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try scrubbing using PNG package
|
||||||
|
pngParser := pis.NewPngMediaParser()
|
||||||
|
if pngParser.LooksLikeFormat(fileData) {
|
||||||
|
intfc, err := pngParser.ParseBytes(fileData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks := intfc.(*pis.ChunkSlice)
|
||||||
|
rootIfd, _, err := chunks.Exif()
|
||||||
|
if err != nil {
|
||||||
|
if err == exif.ErrNoExif {
|
||||||
|
// Incoming data contained no EXIF in the first place so we can return the original
|
||||||
|
return fileData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredIb, err := scrubber.filteringIfdBuilder(rootIfd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
chunks.SetExif(filteredIb)
|
||||||
|
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
err = chunks.WriteTo(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't know how to handle other file formats, so we let the caller decide how to continue
|
||||||
|
return nil, ErrUnknownFileType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether the tag represented by `tag` is included in the path or tag ID list
|
||||||
|
func (scrubber *ExifScrubber) isTagAllowed(tag *exif.IfdTagEntry) bool {
|
||||||
|
// Check via IDs first (faster than string comparisons)
|
||||||
|
for _, includedId := range scrubber.includedTagIds {
|
||||||
|
if includedId == tag.TagId() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no IDs matched, also check IFD tag paths for inclusion
|
||||||
|
tagPath := fmt.Sprintf("%s/%s", tag.IfdPath(), tag.TagName())
|
||||||
|
|
||||||
|
for _, includedPath := range scrubber.includedTagPaths {
|
||||||
|
if includedPath == tagPath {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method follows the implementation of exif.NewIfdBuilderFromExistingChain()
|
||||||
|
func (scrubber *ExifScrubber) filteringIfdBuilder(rootIfd *exif.Ifd) (
|
||||||
|
firstIb *exif.IfdBuilder,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
var lastIb *exif.IfdBuilder
|
||||||
|
i := 0
|
||||||
|
for thisExistingIfd := rootIfd; thisExistingIfd != nil; thisExistingIfd = thisExistingIfd.NextIfd() {
|
||||||
|
// This only works when no non-standard mappings are used
|
||||||
|
ifdMapping, err := exifcommon.NewIfdMappingWithStandard()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This only works when no non-standard tags are used
|
||||||
|
tagIndex := exif.NewTagIndex()
|
||||||
|
err = exif.LoadStandardTags(tagIndex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newIb := exif.NewIfdBuilder(
|
||||||
|
ifdMapping,
|
||||||
|
tagIndex,
|
||||||
|
thisExistingIfd.IfdIdentity(),
|
||||||
|
thisExistingIfd.ByteOrder(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if firstIb == nil {
|
||||||
|
firstIb = newIb
|
||||||
|
} else {
|
||||||
|
lastIb.SetNextIb(newIb)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scrubber.filteredAddTagsFromExisting(newIb, thisExistingIfd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIb = newIb
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstIb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method follows the implementation of exif.IfdBuilder.AddTagsFromExisting()
|
||||||
|
func (scrubber *ExifScrubber) filteredAddTagsFromExisting(
|
||||||
|
ib *exif.IfdBuilder,
|
||||||
|
ifd *exif.Ifd,
|
||||||
|
) (err error) {
|
||||||
|
for i, ite := range ifd.Entries() {
|
||||||
|
if ite.IsThumbnailOffset() == true || ite.IsThumbnailSize() {
|
||||||
|
// These will be added on-the-fly when we encode.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var bt *exif.BuilderTag
|
||||||
|
if ite.ChildIfdPath() != "" {
|
||||||
|
// If we want to add an IFD tag, we'll have to build it first and
|
||||||
|
// *then* add it via a different method.
|
||||||
|
|
||||||
|
// Figure out which of the child-IFDs that are associated with
|
||||||
|
// this IFD represents this specific child IFD.
|
||||||
|
|
||||||
|
var childIfd *exif.Ifd
|
||||||
|
for _, thisChildIfd := range ifd.Children() {
|
||||||
|
if thisChildIfd.ParentTagIndex() != i {
|
||||||
|
continue
|
||||||
|
} else if thisChildIfd.IfdIdentity().TagId() != 0xffff &&
|
||||||
|
thisChildIfd.IfdIdentity().TagId() != ite.TagId() {
|
||||||
|
fmt.Printf(
|
||||||
|
"child-IFD tag is not correct: TAG-POSITION=(%d) ITE=%s CHILD-IFD=%s\n",
|
||||||
|
thisChildIfd.ParentTagIndex(),
|
||||||
|
ite,
|
||||||
|
thisChildIfd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
childIfd = thisChildIfd
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if childIfd == nil {
|
||||||
|
childTagIds := make([]string, len(ifd.Children()))
|
||||||
|
for j, childIfd := range ifd.Children() {
|
||||||
|
childTagIds[j] = fmt.Sprintf(
|
||||||
|
"0x%04x (parent tag-position %d)",
|
||||||
|
childIfd.IfdIdentity().TagId(),
|
||||||
|
childIfd.ParentTagIndex(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"could not find child IFD for child ITE: IFD-PATH=[%s] TAG-ID=(0x%04x) "+
|
||||||
|
"CURRENT-TAG-POSITION=(%d) CHILDREN=%v\n",
|
||||||
|
ite.IfdPath(),
|
||||||
|
ite.TagId(),
|
||||||
|
i,
|
||||||
|
childTagIds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
childIb, err := scrubber.filteringIfdBuilder(childIfd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bt = ib.NewBuilderTagFromBuilder(childIb)
|
||||||
|
} else {
|
||||||
|
// Non-IFD tag.
|
||||||
|
isAllowed := scrubber.isTagAllowed(ite)
|
||||||
|
if !isAllowed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBytes, err := ite.GetRawBytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
value := exif.NewIfdBuilderTagValueFromBytes(rawBytes)
|
||||||
|
bt = exif.NewBuilderTag(
|
||||||
|
ifd.IfdIdentity().UnindexedString(),
|
||||||
|
ite.TagId(),
|
||||||
|
ite.TagType(),
|
||||||
|
value,
|
||||||
|
ifd.ByteOrder(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bt.Value().IsBytes() {
|
||||||
|
err := ib.Add(bt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if bt.Value().IsIb() {
|
||||||
|
err := ib.AddChildIb(bt.Value().Ib())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
116
exifscrubber/exifscrubber_test.go
Normal file
116
exifscrubber/exifscrubber_test.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package exifscrubber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
|
exif "github.com/dsoprea/go-exif/v3"
|
||||||
|
jis "github.com/dsoprea/go-jpeg-image-structure/v2"
|
||||||
|
pis "github.com/dsoprea/go-png-image-structure/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJpgFromFile(t *testing.T) {
|
||||||
|
// Read and strip original file
|
||||||
|
buf, err := ioutil.ReadFile("../fixtures/gps.jpg")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not open file")
|
||||||
|
}
|
||||||
|
|
||||||
|
includeTagIds := []uint16{0x9209} // ID of "Flash" tag
|
||||||
|
includedPaths := []string{
|
||||||
|
"IFD/Orientation",
|
||||||
|
"IFD/GPSInfo/GPSTimeStamp",
|
||||||
|
"IFD/GPSInfo/GPSDateStamp",
|
||||||
|
}
|
||||||
|
|
||||||
|
scrubber := NewExifScrubber(includeTagIds[:], includedPaths[:])
|
||||||
|
|
||||||
|
updatedBuf, err := scrubber.ScrubExif(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether updated file only contains the specified paths
|
||||||
|
intfc, err := jis.NewJpegMediaParser().ParseBytes(updatedBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
sl := intfc.(*jis.SegmentList)
|
||||||
|
rootIfd, _, err := sl.Exif()
|
||||||
|
|
||||||
|
visitor := func(ifd *exif.Ifd, ite *exif.IfdTagEntry) error {
|
||||||
|
tagId := ite.TagId()
|
||||||
|
tagPath := ite.IfdPath() + "/" + ite.TagName()
|
||||||
|
|
||||||
|
tagContained := slices.Contains(includeTagIds, tagId)
|
||||||
|
pathContained := slices.Contains(includedPaths, tagPath)
|
||||||
|
contained := tagContained || pathContained
|
||||||
|
|
||||||
|
if !contained {
|
||||||
|
t.Errorf(
|
||||||
|
"tag %s (%d) included in EXIF although it hasn't been specified",
|
||||||
|
tagPath,
|
||||||
|
tagId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rootIfd.EnumerateTagsRecursively(visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPngFromFile(t *testing.T) {
|
||||||
|
buf, err := ioutil.ReadFile("../fixtures/gps.png")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not open file")
|
||||||
|
}
|
||||||
|
|
||||||
|
includeTagIds := []uint16{0x9209} // ID of "Flash" tag
|
||||||
|
includedPaths := []string{
|
||||||
|
"IFD/Orientation",
|
||||||
|
"IFD/GPSInfo/GPSTimeStamp",
|
||||||
|
"IFD/GPSInfo/GPSDateStamp",
|
||||||
|
}
|
||||||
|
|
||||||
|
scrubber := NewExifScrubber(includeTagIds[:], includedPaths[:])
|
||||||
|
|
||||||
|
updatedBuf, err := scrubber.ScrubExif(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether updated file only contains the specified paths
|
||||||
|
intfc, err := pis.NewPngMediaParser().ParseBytes(updatedBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := intfc.(*pis.ChunkSlice)
|
||||||
|
rootIfd, _, err := cs.Exif()
|
||||||
|
|
||||||
|
visitor := func(ifd *exif.Ifd, ite *exif.IfdTagEntry) error {
|
||||||
|
tagId := ite.TagId()
|
||||||
|
tagPath := ite.IfdPath() + "/" + ite.TagName()
|
||||||
|
|
||||||
|
tagContained := slices.Contains(includeTagIds, tagId)
|
||||||
|
pathContained := slices.Contains(includedPaths, tagPath)
|
||||||
|
contained := tagContained || pathContained
|
||||||
|
|
||||||
|
if !contained {
|
||||||
|
t.Errorf(
|
||||||
|
"tag %s (%d) included in EXIF although it hasn't been specified",
|
||||||
|
tagPath,
|
||||||
|
tagId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rootIfd.EnumerateTagsRecursively(visitor)
|
||||||
|
}
|
BIN
fixtures/gps.jpg
Normal file
BIN
fixtures/gps.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 193 KiB |
BIN
fixtures/gps.png
Normal file
BIN
fixtures/gps.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
19
go.mod
19
go.mod
|
@ -1,3 +1,22 @@
|
||||||
module github.com/leon-richardt/jaf
|
module github.com/leon-richardt/jaf
|
||||||
|
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e
|
||||||
|
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836
|
||||||
|
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d
|
||||||
|
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect
|
||||||
|
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||||
|
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c // indirect
|
||||||
|
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
|
||||||
|
github.com/go-errors/errors v1.1.1 // indirect
|
||||||
|
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
|
||||||
|
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d // indirect
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||||
|
)
|
||||||
|
|
51
go.sum
Normal file
51
go.sum
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||||
|
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
|
||||||
|
github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
|
||||||
|
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e h1:E4XTSQZF/JtOQWcSaJBJho7t+RNWfdO92W/5skg10Jk=
|
||||||
|
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
|
||||||
|
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4=
|
||||||
|
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
|
||||||
|
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836 h1:KGCiMMWxODEMmI3+9Ms04l73efoqFVNKKKPbVyOvKrU=
|
||||||
|
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836/go.mod h1:WaARaUjQuSuDCDFAiU/GwzfxMTJBulfEhqEA2Tx6B4Y=
|
||||||
|
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
|
||||||
|
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||||
|
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
|
||||||
|
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||||
|
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c=
|
||||||
|
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
|
||||||
|
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d h1:2zNIgrJTspLxUKoJGl0Ln24+hufPKSjP3cu4++5MeSE=
|
||||||
|
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d/go.mod h1:scnx0wQSM7UiCMK66dSdiPZvL2hl6iF5DvpZ7uT59MY=
|
||||||
|
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
|
||||||
|
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
|
||||||
|
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
|
||||||
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||||
|
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||||
|
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||||
|
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
|
||||||
|
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||||
|
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
|
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc=
|
||||||
|
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
|
||||||
|
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
17
jaf.go
17
jaf.go
|
@ -7,13 +7,13 @@ import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/leon-richardt/jaf/exifscrubber"
|
||||||
)
|
)
|
||||||
|
|
||||||
const allowedChars = "0123456789ABCDEFGHIJKLMNOPQRTSUVWXYZabcdefghijklmnopqrstuvwxyz"
|
const allowedChars = "0123456789ABCDEFGHIJKLMNOPQRTSUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
var (
|
var config Config
|
||||||
config Config
|
|
||||||
)
|
|
||||||
|
|
||||||
type parameters struct {
|
type parameters struct {
|
||||||
configFile string
|
configFile string
|
||||||
|
@ -40,6 +40,15 @@ func main() {
|
||||||
log.Fatalf("could not read config file: %s\n", err.Error())
|
log.Fatalf("could not read config file: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handler := uploadHandler{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ScrubExif {
|
||||||
|
scrubber := exifscrubber.NewExifScrubber(config.ExifAllowedIds, config.ExifAllowedPaths)
|
||||||
|
handler.exifScrubber = &scrubber
|
||||||
|
}
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
uploadServer := &http.Server{
|
uploadServer := &http.Server{
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
|
@ -48,6 +57,6 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("starting jaf on port %d\n", config.Port)
|
log.Printf("starting jaf on port %d\n", config.Port)
|
||||||
http.Handle("/upload", &uploadHandler{config: config})
|
http.Handle("/upload", &handler)
|
||||||
uploadServer.ListenAndServe()
|
uploadServer.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,17 +5,19 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/leon-richardt/jaf/exifscrubber"
|
||||||
)
|
)
|
||||||
|
|
||||||
type uploadHandler struct {
|
type uploadHandler struct {
|
||||||
config *Config
|
config *Config
|
||||||
|
exifScrubber *exifscrubber.ExifScrubber
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *uploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (handler *uploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
uploadFile, header, err := r.FormFile("file")
|
uploadFile, header, err := r.FormFile("file")
|
||||||
|
@ -24,10 +26,47 @@ func (h *uploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Println(" could not read uploaded file: " + err.Error())
|
log.Println(" could not read uploaded file: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer uploadFile.Close()
|
|
||||||
|
fileData, err := io.ReadAll(uploadFile)
|
||||||
|
uploadFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "could not read attached file: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Println(" could not read attached file: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub EXIF, if requested and detectable by us
|
||||||
|
if handler.config.ScrubExif {
|
||||||
|
scrubbedData, err := handler.exifScrubber.ScrubExif(fileData[:])
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// If scrubbing was successful, update what to write to file
|
||||||
|
fileData = scrubbedData
|
||||||
|
} else {
|
||||||
|
// Unknown file types (not PNG or JPEG) are allowed to contain EXIF, as we don't know
|
||||||
|
// how to handle them. Handling of other errors depends on configuration.
|
||||||
|
if err != exifscrubber.ErrUnknownFileType {
|
||||||
|
if handler.config.ExifAbortOnError {
|
||||||
|
log.Printf("could not scrub EXIF from file, aborting upload: %s", err.Error())
|
||||||
|
http.Error(
|
||||||
|
w,
|
||||||
|
"could not scrub EXIF from file: "+err.Error(),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// An error occured but we are configured to proceed with the upload anyway
|
||||||
|
log.Printf(
|
||||||
|
"could not scrub EXIF from file but proceeding with upload as configured: %s",
|
||||||
|
err.Error(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, fileExtension := splitFileName(header.Filename)
|
_, fileExtension := splitFileName(header.Filename)
|
||||||
link, err := generateLink(h, &uploadFile, fileExtension)
|
link, err := generateLink(handler, fileData[:], fileExtension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "could not save file: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "could not save file: "+err.Error(), http.StatusInternalServerError)
|
||||||
log.Println(" could not save file: " + err.Error())
|
log.Println(" could not save file: " + err.Error())
|
||||||
|
@ -41,7 +80,7 @@ func (h *uploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// Generates a valid link to uploadFile with the specified file extension.
|
// Generates a valid link to uploadFile with the specified file extension.
|
||||||
// Returns the link or an error in case of failure.
|
// Returns the link or an error in case of failure.
|
||||||
// Does not close the passed file pointer.
|
// Does not close the passed file pointer.
|
||||||
func generateLink(handler *uploadHandler, uploadFile *multipart.File, fileExtension string) (string, error) {
|
func generateLink(handler *uploadHandler, fileData []byte, fileExtension string) (string, error) {
|
||||||
// Find an unused file name
|
// Find an unused file name
|
||||||
var fullFileName string
|
var fullFileName string
|
||||||
var savePath string
|
var savePath string
|
||||||
|
@ -57,7 +96,7 @@ func generateLink(handler *uploadHandler, uploadFile *multipart.File, fileExtens
|
||||||
|
|
||||||
link := handler.config.LinkPrefix + fullFileName
|
link := handler.config.LinkPrefix + fullFileName
|
||||||
|
|
||||||
err := saveFile(uploadFile, savePath)
|
err := saveFile(fileData[:], savePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -65,24 +104,13 @@ func generateLink(handler *uploadHandler, uploadFile *multipart.File, fileExtens
|
||||||
return link, nil
|
return link, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFile(data *multipart.File, name string) error {
|
func saveFile(fileData []byte, name string) error {
|
||||||
file, err := os.Create(name)
|
err := os.WriteFile(name, fileData, 0o644)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(file, *data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileExists(filename string) bool {
|
func fileExists(fileName string) bool {
|
||||||
_, err := os.Stat(filename)
|
_, err := os.Stat(fileName)
|
||||||
|
|
||||||
return !errors.Is(err, os.ErrNotExist)
|
return !errors.Is(err, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue